Files
BMS/bms-front/src/views/data/exchangerate/index.vue
T
2026-09-14 17:12:16 +08:00

431 lines
16 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.
<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>