feat(platform): close AI expense value loop

Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
caoxiaozhu
2026-07-17 14:14:08 +08:00
parent 242d68c36f
commit 787bc3a481
507 changed files with 82072 additions and 6344 deletions

View File

@@ -0,0 +1,169 @@
<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>