This commit is contained in:
wuweihua
2026-09-14 18:56:36 +08:00
18 changed files with 1747 additions and 55 deletions
+67
View File
@@ -0,0 +1,67 @@
import request from '@/utils/request'
// 查询汇率列表
export function listExchangeRate(query) {
return request({
url: '/data/exchangerate/list',
method: 'get',
params: query
})
}
// 查询汇率详细
export function getExchangeRate(rateId) {
return request({
url: '/data/exchangerate/' + rateId,
method: 'get'
})
}
// 新增汇率
export function addExchangeRate(data) {
return request({
url: '/data/exchangerate',
method: 'post',
data: data
})
}
// 修改汇率
export function updateExchangeRate(data) {
return request({
url: '/data/exchangerate',
method: 'put',
data: data
})
}
// 删除汇率
export function delExchangeRate(rateIds) {
return request({
url: '/data/exchangerate/' + rateIds,
method: 'delete'
})
}
/**
* 汇率换算
* @param {Object} params amount/fromCurrency/toCurrency/rateType/bizDate
*/
export function convertExchangeRate(params) {
return request({
url: '/data/exchangerate/convert',
method: 'get',
params: params
})
}
/**
* 任意币种金额折算本位币(CNY)
*/
export function convertToBase(params) {
return request({
url: '/data/exchangerate/toBase',
method: 'get',
params: params
})
}
@@ -10,10 +10,7 @@
</div>
<div class="head-code">客户编码:{{ customer.customerCode || route.query.customerCode || '-' }}</div>
<div class="head-right">
<span style="color:#909399;font-size:13px;">币种</span>
<el-select v-model="currentCurrency" style="width: 110px;" @change="loadAccounts">
<el-option v-for="acc in accountList" :key="acc.currency" :label="acc.currency" :value="acc.currency" />
</el-select>
<el-tag type="success" size="small">本位币账户 CNY</el-tag>
</div>
</div>
</el-card>
@@ -22,7 +19,7 @@
<el-row :gutter="12" class="balance-row">
<el-col :xs="12" :sm="8" :md="4">
<el-card shadow="hover" class="balance-card">
<div class="balance-label">账户余额</div>
<div class="balance-label">账户余额(CNY)</div>
<div class="balance-value primary">{{ formatMoney(currentAccount.balance) }}</div>
</el-card>
</el-col>
@@ -101,12 +98,20 @@
<el-tag :type="receiptTypeTag(scope.row.receiptType)">{{ formatOption(receiptTypeOptions, scope.row.receiptType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="收款金额" align="center" width="130">
<el-table-column label="原币金额" align="center" width="130">
<template #default="scope">
<span class="money-in">{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}</span>
<span>{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}</span>
</template>
</el-table-column>
<el-table-column label="待核销" align="center" prop="unallocatedAmount" width="110" />
<el-table-column label="汇率" align="center" width="110">
<template #default="scope">{{ formatRate(scope.row.exchangeRate) }}</template>
</el-table-column>
<el-table-column label="本位币金额(CNY)" align="center" width="140">
<template #default="scope">
<span class="money-in">{{ formatMoney(scope.row.baseAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="待核销(CNY)" align="center" prop="unallocatedAmount" width="110" />
<el-table-column label="收款方式" align="center" prop="payMethod" width="110">
<template #default="scope">{{ formatOption(payMethodOptions, scope.row.payMethod) }}</template>
</el-table-column>
@@ -189,16 +194,23 @@
<el-tag v-else type="danger">支出</el-tag>
</template>
</el-table-column>
<el-table-column label="变动金额" align="center" width="130">
<el-table-column label="原币金额" align="center" width="130">
<template #default="scope">
{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}
</template>
</el-table-column>
<el-table-column label="汇率" align="center" width="100">
<template #default="scope">{{ formatRate(scope.row.exchangeRate) }}</template>
</el-table-column>
<el-table-column label="本位币变动(CNY)" align="center" width="140">
<template #default="scope">
<span :class="scope.row.direction === 1 ? 'money-in' : 'money-out'">
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(scope.row.amount) }}
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(scope.row.baseAmount != null ? scope.row.baseAmount : scope.row.amount) }}
</span>
</template>
</el-table-column>
<el-table-column label="变动前余额" align="center" prop="beforeBalance" width="120" />
<el-table-column label="变动后余额" align="center" prop="afterBalance" width="120" />
<el-table-column label="币种" align="center" prop="currency" width="70" />
<el-table-column label="关联单号" align="center" prop="bizNo" width="180" show-overflow-tooltip />
<el-table-column label="操作人" align="center" prop="createBy" width="100" />
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip min-width="160" />
@@ -232,8 +244,8 @@
</el-col>
<el-col :span="12">
<el-form-item label="收款金额" prop="amount">
<el-input v-model="receiptDialog.form.amount" placeholder="请输入金额" type="number">
<template #prepend>¥</template>
<el-input v-model="receiptDialog.form.amount" placeholder="请输入原币金额" type="number">
<template #prepend>{{ receiptDialog.form.currency || 'CNY' }}</template>
</el-input>
</el-form-item>
</el-col>
@@ -246,13 +258,33 @@
</el-col>
<el-col :span="12">
<el-form-item label="币种" prop="currency">
<el-select v-model="receiptDialog.form.currency" placeholder="请选择" style="width: 100%;">
<el-option label="人民币 CNY" value="CNY" />
<el-option label="美元 USD" value="USD" />
<el-option label="欧元 EUR" value="EUR" />
<el-select v-model="receiptDialog.form.currency" placeholder="请选择" style="width: 100%;" @change="handleCurrencyChange">
<el-option v-for="c in currencyOptions" :key="c.value" :label="c.label" :value="c.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="汇率" prop="exchangeRate">
<el-input-number
v-model="receiptDialog.form.exchangeRate"
:precision="8"
:step="0.0001"
:min="0"
:disabled="receiptDialog.form.currency === 'CNY'"
controls-position="right"
style="width: 100%;"
@input="handleRateInput"
/>
<div class="rate-tip">1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY<span v-if="receiptDialog.form.currency !== 'CNY'">,默认取汇率表,可手工修改</span></div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="本位币金额">
<el-input :model-value="'CNY ' + baseAmountPreview" disabled>
<template #prepend>≈</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="收款方式" prop="payMethod">
<el-select v-model="receiptDialog.form.payMethod" placeholder="请选择" style="width: 100%;" clearable>
@@ -321,10 +353,14 @@
<el-descriptions-item label="收款类型">
<el-tag :type="receiptTypeTag(viewDialog.data.receiptType)">{{ formatOption(receiptTypeOptions, viewDialog.data.receiptType) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="收款金额">
<el-descriptions-item label="原币金额">
<span class="money-in">{{ viewDialog.data.currency }} {{ formatMoney(viewDialog.data.amount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="待核销金额">{{ formatMoney(viewDialog.data.unallocatedAmount) }}</el-descriptions-item>
<el-descriptions-item label="对CNY汇率">{{ formatRate(viewDialog.data.exchangeRate) }}</el-descriptions-item>
<el-descriptions-item label="本位币金额">
<span class="money-in">CNY {{ formatMoney(viewDialog.data.baseAmount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="待核销金额">CNY {{ formatMoney(viewDialog.data.unallocatedAmount) }}</el-descriptions-item>
<el-descriptions-item label="收款方式">{{ formatOption(payMethodOptions, viewDialog.data.payMethod) }}</el-descriptions-item>
<el-descriptions-item label="银行流水号">{{ viewDialog.data.bankTransactionNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="确认时间">{{ formatDateTime(viewDialog.data.confirmedAt) }}</el-descriptions-item>
@@ -368,6 +404,7 @@ import axios from 'axios'
import { saveAs } from 'file-saver'
import { getCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js"
import { listAccount, listReceipt, getReceipt, addReceipt, confirmReceipt, cancelReceipt, listFlow } from "@/api/customersuppliermanagement/customermoney/customermoney.js"
import { convertToBase } from "@/api/data/exchangeRate"
import { getToken } from "@/utils/auth"
import { useRoute } from 'vue-router'
@@ -406,6 +443,15 @@ const bizTypeOptions = [
{ value: 'FREEZE', label: '冻结' },
{ value: 'UNFREEZE', label: '解冻' }
]
/** 登记收款可选币种(汇率需在 数据管理-汇率管理 维护对CNY汇率) */
const currencyOptions = [
{ value: 'CNY', label: '人民币 CNY' },
{ value: 'USD', label: '美元 USD' },
{ value: 'EUR', label: '欧元 EUR' },
{ value: 'GBP', label: '英镑 GBP' },
{ value: 'HKD', label: '港币 HKD' },
{ value: 'JPY', label: '日元 JPY' }
]
@@ -427,17 +473,22 @@ function formatMoney(val) {
const num = Number(val)
return isNaN(num) ? '0.00' : num.toFixed(2)
}
/** 汇率展示:去尾零,最多8位小数(7.10000000 → 7.1) */
function formatRate(val) {
if (val === null || val === undefined || val === '') return '1'
const num = Number(val)
return isNaN(num) ? String(val) : String(num)
}
function formatDateTime(val) {
if (!val) return '-'
return String(val).length > 16 ? String(val).substring(0, 16) : val
}
/* ================= 客户信息 & 账户 ================= */
/* ================= 客户信息 & 账户(统一本位币 CNY) ================= */
const accountList = ref([])
const currentCurrency = ref('CNY')
const currentAccount = computed(() => {
return accountList.value.find(a => a.currency === currentCurrency.value) || {}
return accountList.value.find(a => a.currency === 'CNY') || {}
})
/** 可用额度 = 总额度 + 额外额度 - 已用额度 */
@@ -462,10 +513,6 @@ function loadAccounts() {
if (!customerId) return
listAccount(customerId).then(res => {
accountList.value = res.data || []
if (accountList.value.length > 0) {
const exists = accountList.value.some(a => a.currency === currentCurrency.value)
if (!exists) currentCurrency.value = accountList.value[0].currency
}
})
}
@@ -542,12 +589,14 @@ const receiptRules = {
}
function resetReceiptForm() {
rateEdited.value = false
receiptDialog.form = {
customerId: route.query.customerId || null,
receivedAt: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
amount: '',
receiptType: 'PREPAY',
currency: 'CNY',
exchangeRate: 1,
payMethod: 'BANK_TRANSFER',
bankTransactionNo: '',
remark: '',
@@ -561,9 +610,65 @@ function handleAddReceipt() {
receiptDialog.visible = true
}
/** 汇率是否被手工修改过(修改后切换币种/日期不再自动覆盖) */
const rateEdited = ref(false)
/** 按收款日期从汇率表取该币种对CNY即期汇率 */
function fetchDefaultRate() {
const cur = receiptDialog.form.currency
if (!cur || cur === 'CNY') {
receiptDialog.form.exchangeRate = 1
return
}
const bizDate = receiptDialog.form.receivedAt
? String(receiptDialog.form.receivedAt).substring(0, 10)
: proxy.parseTime(new Date(), '{y}-{m}-{d}')
convertToBase({ amount: 1, currency: cur, rateType: '0', bizDate })
.then(res => {
if (!rateEdited.value) receiptDialog.form.exchangeRate = Number(res.data.rate)
})
.catch(() => {
if (!rateEdited.value) {
receiptDialog.form.exchangeRate = undefined
proxy.$modal.msgWarning(`汇率表中未维护 ${cur} 对 CNY 的汇率,请手工录入汇率`)
}
})
}
/** 切换币种:重置手工标记并重新取默认汇率 */
function handleCurrencyChange() {
rateEdited.value = false
fetchDefaultRate()
}
/** 手工修改汇率 */
function handleRateInput() {
if (receiptDialog.form.currency !== 'CNY') rateEdited.value = true
}
/** 本位币金额预览 = 原币金额 × 汇率 */
const baseAmountPreview = computed(() => {
const amount = Number(receiptDialog.form.amount) || 0
const rate = Number(receiptDialog.form.exchangeRate) || 0
return formatMoney(amount * rate)
})
// 收款日期变化且汇率未手改时,按新日期重新取汇率表
watch(() => receiptDialog.form.receivedAt, () => {
if (receiptDialog.visible && receiptDialog.form.currency !== 'CNY' && !rateEdited.value) {
fetchDefaultRate()
}
})
function submitReceipt() {
receiptFormRef.value.validate(valid => {
if (!valid) return
const cur = receiptDialog.form.currency
const rate = Number(receiptDialog.form.exchangeRate)
if (cur !== 'CNY' && (!rate || rate <= 0)) {
proxy.$modal.msgError(`请录入 ${cur} 对 CNY 的汇率(汇率表未配置时可手工填写)`)
return
}
submitLoading.value = true
addReceipt(receiptDialog.form).then(() => {
proxy.$modal.msgSuccess("登记成功,等待审核")
@@ -729,7 +834,6 @@ function initPage() {
// 重置页面状态,避免上一个客户的数据残留
customer.value = {}
accountList.value = []
currentCurrency.value = 'CNY'
activeTab.value = 'receipt'
receiptDialog.visible = false
viewDialog.visible = false
@@ -815,6 +919,12 @@ watch(() => route.query.customerId, (newId, oldId) => {
.balance-value.success { color: var(--el-color-success); }
.balance-value.warning { color: var(--el-color-warning); }
.balance-value.danger { color: var(--el-color-danger); }
.rate-tip {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.4;
}
.search-form {
margin-bottom: -4px;
}
@@ -185,12 +185,9 @@
<el-table-column label="客服姓名" align="center" prop="serviceUser" />
<el-table-column label="结算方式" align="center" prop="settlementType" />
<el-table-column label="临时额度" align="center" prop="tempEd" />
<el-table-column label="账户余额" align="center" prop="accountBalance" width="110">
<el-table-column label="账户余额(CNY)" align="center" prop="accountBalance" width="130">
<template #default="scope">{{ formatBalance(scope.row.accountBalance) }}</template>
</el-table-column>
<el-table-column label="外币余额" align="center" prop="foreignCurrencyBalance" width="110">
<template #default="scope">{{ formatBalance(scope.row.foreignCurrencyBalance) }}</template>
</el-table-column>
<el-table-column label="重量阈值" align="center" prop="weightThreshold" />
<el-table-column label="抛比" align="center" prop="throwRatio" />
<el-table-column label="信用额度" align="center" prop="creditEd" />
@@ -315,6 +312,20 @@
<el-input v-model="form.ed" placeholder="请输入资金额度" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结算方式" prop="settlementType">
<el-select
v-model="form.settlementType"
placeholder="请选择结算方式"
clearable
>
<el-option label="月结" value="0" />
<el-option label="周结" value="1" />
<el-option label="日结" value="2" />
<el-option label="现结" value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="回款账期" prop="zq">
<el-input v-model="form.zq" placeholder="请输入回款账期" />
@@ -364,6 +375,7 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="临时额度" prop="tempEd">
<el-input v-model="form.tempEd" placeholder="请输入临时额度" />
@@ -376,13 +388,6 @@
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="外币余额">
<el-input :model-value="formatBalance(form.foreignCurrencyBalance)" disabled placeholder="0.00">
<template #append>外币</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="重量阈值" prop="weightThreshold">
<el-input v-model="form.weightThreshold" placeholder="请输入重量阈值" />
@@ -0,0 +1,430 @@
<template>
<div class="app-container">
<!-- 汇率试算 -->
<el-card shadow="never" class="calc-card">
<template #header>
<div class="calc-header">
<span class="calc-title">汇率换算 / 本位币(CNY)折算</span>
<el-tag size="small" type="info">取数:正向配置 → 反向取倒数 → 经CNY交叉换算</el-tag>
</div>
</template>
<el-form :inline="true" @submit.prevent>
<el-form-item label="金额">
<el-input-number v-model="calc.amount" :precision="2" :step="100" :min="0" controls-position="right" style="width: 170px;" />
</el-form-item>
<el-form-item label="源币种">
<el-select v-model="calc.fromCurrency" filterable allow-create style="width: 110px;" placeholder="如 USD">
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
</el-select>
</el-form-item>
<el-form-item label="目标币种">
<el-select v-model="calc.toCurrency" filterable allow-create style="width: 110px;" placeholder="如 CNY">
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
</el-select>
</el-form-item>
<el-form-item label="汇率类型">
<el-select v-model="calc.rateType" style="width: 110px;">
<el-option v-for="d in rateTypeOptions" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item label="业务日期">
<el-date-picker v-model="calc.bizDate" type="date" value-format="YYYY-MM-DD" placeholder="默认今天" style="width: 150px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Money" @click="handleConvert">换 算</el-button>
<el-button type="success" plain @click="handleToBase">折本位币(CNY)</el-button>
</el-form-item>
</el-form>
<el-alert v-if="calcResult" :closable="false" type="success" class="calc-result">
<template #title>
<span>{{ calcResult.amount }} {{ calcResult.fromCurrency }}</span>
<span class="calc-eq">=</span>
<span class="calc-amount">{{ calcResult.result }} {{ calcResult.toCurrency }}</span>
<el-divider direction="vertical" />
<span style="font-size: 12px; color: #909399;">
汇率 1 {{ calcResult.fromCurrency }} = {{ calcResult.rate }} {{ calcResult.toCurrency }}
({{ ratePathText(calcResult.ratePath) }},{{ rateTypeText(calcResult.rateType) }},{{ calcResult.bizDate }})
</span>
</template>
</el-alert>
</el-card>
<!-- 查询 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-form-item label="源币种" prop="fromCurrency">
<el-select v-model="queryParams.fromCurrency" filterable allow-create clearable style="width: 120px;" placeholder="全部">
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
</el-select>
</el-form-item>
<el-form-item label="目标币种" prop="toCurrency">
<el-select v-model="queryParams.toCurrency" filterable allow-create clearable style="width: 120px;" placeholder="全部">
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
</el-select>
</el-form-item>
<el-form-item label="汇率类型" prop="rateType">
<el-select v-model="queryParams.rateType" clearable style="width: 120px;" placeholder="全部">
<el-option v-for="d in rateTypeOptions" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item label="来源" prop="source">
<el-input v-model="queryParams.source" placeholder="汇率来源" clearable style="width: 140px;" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="生效日期">
<el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始" end-placeholder="结束" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['data:exchangerate:add']">新增汇率</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['data:exchangerate:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="rateList" border>
<el-table-column label="ID" align="center" prop="rateId" width="70" />
<el-table-column label="币种方向" align="center" width="150">
<template #default="scope">
<el-tag size="small">{{ scope.row.fromCurrency }}</el-tag>
<el-icon class="rate-arrow"><Right /></el-icon>
<el-tag size="small" type="success">{{ scope.row.toCurrency }}</el-tag>
</template>
</el-table-column>
<el-table-column label="类型" align="center" prop="rateType" width="100">
<template #default="scope">
<el-tag :type="rateTypeTag(scope.row.rateType)">{{ rateTypeText(scope.row.rateType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="汇率值(1源=?目标)" align="center" prop="exchangeRate" width="170">
<template #default="scope">
<span class="rate-value">{{ formatRate(scope.row.exchangeRate) }}</span>
</template>
</el-table-column>
<el-table-column label="生效日期" align="center" prop="effectiveDate" width="110" />
<el-table-column label="失效日期" align="center" prop="expireDate" width="110">
<template #default="scope">{{ scope.row.expireDate || '长期' }}</template>
</el-table-column>
<el-table-column label="来源" align="center" prop="source" width="110" />
<el-table-column label="状态" align="center" width="90">
<template #default="scope">
<el-tag :type="isEffective(scope.row) ? 'success' : 'info'" size="small">
{{ isEffective(scope.row) ? '生效中' : '未生效' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip min-width="140" />
<el-table-column label="操作" align="center" width="140" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['data:exchangerate:edit']">修改</el-button>
<el-button link type="danger" size="small" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['data:exchangerate:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 新增/修改弹窗 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="rateRef" :model="form" :rules="rules" label-width="110px">
<el-row>
<el-col :span="12">
<el-form-item label="源币种" prop="fromCurrency">
<el-select v-model="form.fromCurrency" filterable allow-create style="width: 100%;" placeholder="ISO三位码 如 USD">
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标币种" prop="toCurrency">
<el-select v-model="form.toCurrency" filterable allow-create style="width: 100%;" placeholder="ISO三位码 如 CNY">
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="汇率类型" prop="rateType">
<el-select v-model="form.rateType" style="width: 100%;">
<el-option v-for="d in rateTypeOptions" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="汇率值" prop="exchangeRate">
<el-input-number v-model="form.exchangeRate" :precision="8" :step="0.0001" :min="0.00000001" controls-position="right" style="width: 100%;" placeholder="1源币种=?目标币种" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生效日期" prop="effectiveDate">
<el-date-picker v-model="form.effectiveDate" type="date" value-format="YYYY-MM-DD" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="失效日期" prop="expireDate">
<el-date-picker v-model="form.expireDate" type="date" value-format="YYYY-MM-DD" placeholder="空=长期有效" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="汇率来源" prop="source">
<el-input v-model="form.source" placeholder="中国银行/央行/手工录入" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="open = false">取 消</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="ExchangeRate">
import { listRegionOptions } from '@/api/data/region'
import {
listExchangeRate,
getExchangeRate,
addExchangeRate,
updateExchangeRate,
delExchangeRate,
convertExchangeRate,
convertToBase
} from '@/api/data/exchangeRate'
const { proxy } = getCurrentInstance()
const rateList = ref([])
const loading = ref(true)
const showSearch = ref(true)
const total = ref(0)
const dateRange = ref([])
const title = ref('')
const open = ref(false)
const calcResult = ref(null)
const rateTypeOptions = [
{ value: '0', label: '即期汇率' },
{ value: '1', label: '月初汇率' },
{ value: '2', label: '月末汇率' },
{ value: '3', label: '固定汇率' }
]
/** 常用币种兜底 + 从国家资料(币种)去重加载 */
const currencyOptions = ref(['CNY', 'USD', 'EUR', 'GBP', 'HKD', 'JPY'])
function loadCurrencyOptions() {
listRegionOptions().then(res => {
const codes = (res.data || [])
.map(r => r.currencyCode)
.filter(c => !!c)
currencyOptions.value = [...new Set([...currencyOptions.value, ...codes])]
}).catch(() => {})
}
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
fromCurrency: '',
toCurrency: '',
rateType: '',
source: ''
})
const calc = reactive({
amount: 100,
fromCurrency: 'USD',
toCurrency: 'CNY',
rateType: '0',
bizDate: ''
})
const form = ref({})
const rules = {
fromCurrency: [{ required: true, message: '源币种不能为空', trigger: 'change' }],
toCurrency: [{ required: true, message: '目标币种不能为空', trigger: 'change' }],
rateType: [{ required: true, message: '请选择汇率类型', trigger: 'change' }],
exchangeRate: [{ required: true, message: '汇率值不能为空', trigger: 'blur' }],
effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
}
function rateTypeText(v) {
const o = rateTypeOptions.find(d => d.value === String(v))
return o ? o.label : v
}
function rateTypeTag(v) {
return { '0': 'danger', '1': 'warning', '2': '', '3': 'success' }[v] || 'info'
}
function formatRate(v) {
const num = Number(v)
return isNaN(num) ? '-' : num.toFixed(8).replace(/0+$/, '').replace(/\.$/, '')
}
function ratePathText(v) {
return { DIRECT: '正向配置', REVERSE: '反向取倒数', CROSS: '经CNY交叉换算', SAME: '同币种' }[v] || v
}
function isEffective(row) {
const today = proxy.parseTime(new Date(), '{y}-{m}-{d}')
return row.effectiveDate <= today && (!row.expireDate || row.expireDate >= today)
}
function getList() {
loading.value = true
listExchangeRate(proxy.addDateRange({ ...queryParams }, dateRange.value)).then(res => {
rateList.value = res.rows
total.value = res.total
}).finally(() => {
loading.value = false
})
}
function handleQuery() {
queryParams.pageNum = 1
getList()
}
function resetQuery() {
dateRange.value = []
proxy.resetForm('queryRef')
handleQuery()
}
function reset() {
form.value = {
rateId: null,
fromCurrency: 'USD',
toCurrency: 'CNY',
rateType: '0',
exchangeRate: null,
effectiveDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
expireDate: '',
source: '手工录入',
remark: ''
}
proxy.resetForm('rateRef')
}
function handleAdd() {
reset()
open.value = true
title.value = '新增汇率'
}
function handleUpdate(row) {
reset()
getExchangeRate(row.rateId).then(res => {
form.value = {
...res.data,
expireDate: res.data.expireDate || ''
}
open.value = true
title.value = '修改汇率'
})
}
function submitForm() {
proxy.$refs.rateRef.validate(valid => {
if (!valid) return
const data = { ...form.value }
data.fromCurrency = (data.fromCurrency || '').toUpperCase()
data.toCurrency = (data.toCurrency || '').toUpperCase()
if (!data.expireDate) data.expireDate = null
const action = data.rateId ? updateExchangeRate(data) : addExchangeRate(data)
action.then(() => {
proxy.$modal.msgSuccess(data.rateId ? '修改成功' : '新增成功')
open.value = false
getList()
})
})
}
function handleDelete(row) {
proxy.$modal.confirm('是否确认删除汇率【' + row.fromCurrency + ' → ' + row.toCurrency + ' / ' + rateTypeText(row.rateType) + '】?').then(() => {
return delExchangeRate(row.rateId)
}).then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
}).catch(() => {})
}
function handleExport() {
proxy.download('/data/exchangerate/export', { ...queryParams }, `exchange_rate_${new Date().getTime()}.xlsx`)
}
function handleConvert() {
if (!calc.amount && calc.amount !== 0) {
proxy.$modal.msgWarning('请输入金额')
return
}
convertExchangeRate({
amount: calc.amount,
fromCurrency: calc.fromCurrency,
toCurrency: calc.toCurrency,
rateType: calc.rateType,
bizDate: calc.bizDate || undefined
}).then(res => {
calcResult.value = res.data
})
}
function handleToBase() {
if (!calc.amount && calc.amount !== 0) {
proxy.$modal.msgWarning('请输入金额')
return
}
convertToBase({
amount: calc.amount,
currency: calc.fromCurrency,
rateType: calc.rateType,
bizDate: calc.bizDate || undefined
}).then(res => {
calcResult.value = res.data
})
}
loadCurrencyOptions()
getList()
</script>
<style scoped>
.calc-card {
margin-bottom: 12px;
}
.calc-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.calc-title {
font-weight: 600;
}
.calc-result {
margin-top: 4px;
}
.calc-eq {
margin: 0 8px;
color: #909399;
}
.calc-amount {
font-size: 18px;
font-weight: 700;
color: var(--el-color-success);
}
.rate-arrow {
margin: 0 4px;
vertical-align: middle;
color: #909399;
}
.rate-value {
font-weight: 600;
color: var(--el-color-primary);
}
</style>