170 lines
6.3 KiB
Vue
170 lines
6.3 KiB
Vue
|
|
<template>
|
|||
|
|
<el-dialog
|
|||
|
|
:model-value="open"
|
|||
|
|
class="cfo-value-action-dialog"
|
|||
|
|
width="min(92vw, 560px)"
|
|||
|
|
:close-on-click-modal="false"
|
|||
|
|
:close-on-press-escape="!submitting"
|
|||
|
|
:show-close="!submitting"
|
|||
|
|
:title="actionMeta.label"
|
|||
|
|
@update:model-value="$emit('update:open', $event)"
|
|||
|
|
>
|
|||
|
|
<form class="value-action-form" @submit.prevent="submit">
|
|||
|
|
<div class="value-action-context">
|
|||
|
|
<span>{{ contextLabel }}</span>
|
|||
|
|
<strong>{{ targetTitle }}</strong>
|
|||
|
|
<small>当前版本 v{{ Number(target?.version || 0) }}</small>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<label v-if="action === 'reverse'" class="value-action-field">
|
|||
|
|
<span>冲回金额 <em aria-hidden="true">*</em></span>
|
|||
|
|
<input
|
|||
|
|
v-model="form.reversalAmount"
|
|||
|
|
type="number"
|
|||
|
|
min="0.01"
|
|||
|
|
step="0.01"
|
|||
|
|
inputmode="decimal"
|
|||
|
|
autocomplete="off"
|
|||
|
|
/>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<label class="value-action-field">
|
|||
|
|
<span>操作说明 <em aria-hidden="true">*</em></span>
|
|||
|
|
<textarea
|
|||
|
|
v-model.trim="form.comment"
|
|||
|
|
rows="4"
|
|||
|
|
maxlength="1000"
|
|||
|
|
:placeholder="commentPlaceholder"
|
|||
|
|
></textarea>
|
|||
|
|
<small>至少 2 个字符;说明将写入不可变事件记录。</small>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<p v-if="validationError || error" class="value-action-error" role="alert">
|
|||
|
|
<i class="mdi mdi-alert-circle-outline" aria-hidden="true"></i>
|
|||
|
|
{{ validationError || error?.message || '操作失败,请刷新后重试。' }}
|
|||
|
|
</p>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<template #footer>
|
|||
|
|
<div class="value-action-footer">
|
|||
|
|
<button type="button" class="btn ghost" :disabled="submitting" @click="$emit('update:open', false)">
|
|||
|
|
取消
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
class="btn"
|
|||
|
|
:class="actionMeta.tone === 'danger' ? 'danger' : 'primary'"
|
|||
|
|
:disabled="submitting"
|
|||
|
|
@click="submit"
|
|||
|
|
>
|
|||
|
|
<i v-if="submitting" class="mdi mdi-loading mdi-spin" aria-hidden="true"></i>
|
|||
|
|
{{ submitting ? '正在写入证据链' : actionMeta.label }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</el-dialog>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed, reactive, ref, watch } from 'vue'
|
|||
|
|
|
|||
|
|
import { createRequestId, resolveActionMeta } from '../../views/scripts/cfoValueDashboardModel.js'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
open: { type: Boolean, default: false },
|
|||
|
|
action: { type: String, default: '' },
|
|||
|
|
target: { type: Object, default: () => null },
|
|||
|
|
opportunity: { type: Object, default: () => null },
|
|||
|
|
submitting: { type: Boolean, default: false },
|
|||
|
|
error: { type: Object, default: () => null }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['update:open', 'submit'])
|
|||
|
|
const form = reactive(createForm())
|
|||
|
|
const validationError = ref('')
|
|||
|
|
const actionMeta = computed(() => {
|
|||
|
|
const meta = resolveActionMeta(props.action)
|
|||
|
|
if (props.target?.realizationType || props.target?.realizationKey) {
|
|||
|
|
return { ...meta, kind: 'realization' }
|
|||
|
|
}
|
|||
|
|
return meta
|
|||
|
|
})
|
|||
|
|
const targetTitle = computed(() => (
|
|||
|
|
props.opportunity?.title
|
|||
|
|
|| props.target?.title
|
|||
|
|
|| props.opportunity?.claimNoSnapshot
|
|||
|
|
|| props.target?.id
|
|||
|
|
|| '节省机会'
|
|||
|
|
))
|
|||
|
|
const contextLabel = computed(() => (
|
|||
|
|
actionMeta.value.kind === 'realization' ? '实际结果' : '节省机会'
|
|||
|
|
))
|
|||
|
|
const commentPlaceholder = computed(() => {
|
|||
|
|
if (props.action === 'confirm') return '请说明核验依据或外部凭证来源'
|
|||
|
|
if (props.action === 'reverse') return '请说明冲回原因及对应事实'
|
|||
|
|
if (props.action === 'reject') return '请说明拒绝原因'
|
|||
|
|
return '请说明本次操作依据'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
() => [props.open, props.action, props.target?.id],
|
|||
|
|
([open]) => {
|
|||
|
|
if (!open) return
|
|||
|
|
Object.assign(form, createForm(props.target, props.opportunity))
|
|||
|
|
validationError.value = ''
|
|||
|
|
},
|
|||
|
|
{ immediate: true }
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
function submit() {
|
|||
|
|
validationError.value = validate()
|
|||
|
|
if (validationError.value || props.submitting) return
|
|||
|
|
|
|||
|
|
emit('submit', {
|
|||
|
|
kind: actionMeta.value.kind,
|
|||
|
|
action: props.action,
|
|||
|
|
requestId: form.requestId,
|
|||
|
|
opportunityId: props.opportunity?.id || props.target?.id,
|
|||
|
|
realizationId: actionMeta.value.kind === 'realization' ? props.target?.id : undefined,
|
|||
|
|
expectedVersion: Number(props.target?.version || 0),
|
|||
|
|
comment: form.comment,
|
|||
|
|
...(props.action === 'reverse' ? { reversalAmount: form.reversalAmount } : {})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function validate() {
|
|||
|
|
if (String(form.comment || '').trim().length < 2) return '请填写至少 2 个字符的操作说明。'
|
|||
|
|
if (Number(props.target?.version || 0) < 1) return '当前记录缺少有效版本,请刷新详情后重试。'
|
|||
|
|
if (props.action === 'reverse' && Number(form.reversalAmount) <= 0) return '冲回金额必须大于 0。'
|
|||
|
|
return ''
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createForm() {
|
|||
|
|
return {
|
|||
|
|
requestId: createRequestId('cfo-value-action'),
|
|||
|
|
comment: '',
|
|||
|
|
reversalAmount: ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.value-action-form { display: grid; gap: 18px; }
|
|||
|
|
.value-action-context { display: grid; gap: 4px; padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-soft); }
|
|||
|
|
.value-action-context span, .value-action-context small { color: var(--muted); font-size: 12px; }
|
|||
|
|
.value-action-context strong { color: var(--ink); font-size: 14px; }
|
|||
|
|
.value-action-field { display: grid; gap: 7px; color: var(--text); font-size: 13px; font-weight: 700; }
|
|||
|
|
.value-action-field em { color: var(--danger); font-style: normal; }
|
|||
|
|
.value-action-field input, .value-action-field textarea { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--surface); color: var(--ink); }
|
|||
|
|
.value-action-field textarea { resize: vertical; line-height: 1.55; }
|
|||
|
|
.value-action-field small { color: var(--muted); font-size: 12px; font-weight: 500; line-height: 1.55; }
|
|||
|
|
.value-action-error { display: flex; align-items: flex-start; gap: 8px; color: var(--danger-active); font-size: 13px; line-height: 1.5; }
|
|||
|
|
.value-action-footer { display: flex; justify-content: flex-end; gap: 10px; }
|
|||
|
|
.value-action-footer .btn { min-width: 112px; min-height: 44px; }
|
|||
|
|
.btn:disabled { cursor: not-allowed; opacity: .48; transform: none; box-shadow: none; }
|
|||
|
|
@media (max-width: 560px) {
|
|||
|
|
.value-action-footer { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|||
|
|
.value-action-footer .btn { width: 100%; min-width: 0; }
|
|||
|
|
}
|
|||
|
|
</style>
|