Files
admin-web/src/modules/overseas-master-data/pages/ExchangeRatesView.vue
T

75 lines
9.6 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.
<script setup lang="ts">
import { useExportTask } from '@/modules/downloads'
import '@/styles/adminDensity.css'
import { onMounted, reactive, ref, watch } from 'vue'
import { Download, Plus } from '@lucide/vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { changeExchangeRateStatus, createExchangeRate, getExchangeRate, getExchangeRateSummary, listExchangeRates, updateExchangeRate } from '../api/overseasMasterData'
import { overseasMasterDataPermissions as permission } from '../permissions'
import { masterDataErrorMessage } from '../apiError'
import type { MasterDataStatus, MonthlyExchangeRate } from '../types'
const submitExport = useExportTask()
const access = useAccessPreviewStore()
const rows = ref<MonthlyExchangeRate[]>([]), total = ref(0), page = ref(1), pageSize = ref(20), loading = ref(false), saving = ref(false), exporting = ref(false)
const error = ref<ApiErrorDetails | null>(null), editorOpen = ref(false), editingMonth = ref('')
const filters = ref<QueryModel>({ month: '', status: '' })
const fields: QueryField[] = [
{ key: 'month', type: 'date', label: '查询月份', placeholder: '请选择月份', width: 200, props: { type: 'month', 'value-format': 'YYYY-MM' } },
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 130, options: [{ label: '启用', value: 'ACTIVE' }, { label: '停用', value: 'INACTIVE' }] },
]
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 230, fixed: 'left', slotName: 'actions' }, { key: 'month', prop: 'month', label: '汇率月份', minWidth: 120 },
{ key: 'usdToCnyAverage', prop: 'usdToCnyAverage', label: '平均汇率', minWidth: 120 }, { key: 'conversion', label: '换算说明', minWidth: 190, slotName: 'conversion' },
{ key: 'status', label: '状态', width: 90, slotName: 'status' }, { key: 'updatedBy', prop: 'updatedBy', label: '更新人', minWidth: 120 },
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 200 },
]
const summary = reactive({ latestMonth: '', latestUsdToCnyAverage: null as number | null, updatedBy: '', updatedAt: '' })
const form = reactive({ month: '', usdToCnyAverage: 0, remark: '', recordVersion: 0 })
function params() { const month = String(filters.value.month || ''); return { page: page.value, size: pageSize.value, monthFrom: month || undefined, monthTo: month || undefined, status: (filters.value.status || undefined) as MasterDataStatus | undefined } }
async function load() { loading.value = true; error.value = null; try { const [result, latest] = await Promise.all([listExchangeRates(params()), getExchangeRateSummary()]); rows.value = result.records; total.value = result.total; Object.assign(summary, latest) } catch (cause) { rows.value = []; total.value = 0; error.value = getApiErrorDetails(cause, '月度汇率加载失败') } finally { loading.value = false } }
function search() { if (page.value === 1) void load(); else page.value = 1 }
function openCreate() { editingMonth.value = ''; Object.assign(form, { month: '', usdToCnyAverage: 0, remark: '', recordVersion: 0 }); editorOpen.value = true }
async function openEdit(row: MonthlyExchangeRate) { try { const value = await getExchangeRate(row.month); editingMonth.value = value.month; Object.assign(form, { month: value.month, usdToCnyAverage: Number(value.usdToCnyAverage), remark: value.remark || '', recordVersion: value.recordVersion }); editorOpen.value = true } catch (cause) { ElMessage.error(masterDataErrorMessage(cause, '汇率详情加载失败')) } }
async function save() { if (!form.month || form.usdToCnyAverage <= 0) { ElMessage.warning('请选择月份并填写大于 0 的平均汇率'); return } saving.value = true; try { if (editingMonth.value) await updateExchangeRate(editingMonth.value, { recordVersion: form.recordVersion, usdToCnyAverage: form.usdToCnyAverage, remark: form.remark || null }); else await createExchangeRate({ month: form.month, usdToCnyAverage: form.usdToCnyAverage, remark: form.remark || null }); ElMessage.success(editingMonth.value ? '月度汇率已更新' : '月度汇率已创建'); editorOpen.value = false; await load() } catch (cause) { ElMessage.error(masterDataErrorMessage(cause, '月度汇率保存失败')) } finally { saving.value = false } }
async function toggle(row: MonthlyExchangeRate) { const next: MasterDataStatus = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'; try { await ElMessageBox.confirm(`确认${next === 'ACTIVE' ? '启用' : '停用'} ${row.month} 汇率吗?`, `${next === 'ACTIVE' ? '启用' : '停用'}月度汇率 · ${row.month}`, { customClass: 'admin-density-confirm', type: 'warning' }); await changeExchangeRateStatus(row.month, next, row.recordVersion, null); ElMessage.success('汇率状态已更新'); await load() } catch (cause) { if (cause !== 'cancel' && cause !== 'close') ElMessage.error(masterDataErrorMessage(cause, '汇率状态更新失败')) } }
async function exportCsv() { exporting.value = true; try { const { page: _page, size: _size, ...query } = params(); await submitExport('MONTHLY_EXCHANGE_RATE_LIST', '月度汇率', query) } catch (cause) { ElMessage.error(masterDataErrorMessage(cause, '汇率导出失败')) } finally { exporting.value = false } }
watch([page, pageSize], load); onMounted(load)
</script>
<template><AdminPage class="admin-density"><ListPageLayout aria-label="汇率管理">
<section><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="true" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="total" :loading="loading" row-key="month" preference-key="overseas-exchange-rates-table" empty-text="暂无月度汇率">
<template #toolbar-start><h1>汇率管理</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="汇率月份筛选" preference-key="overseas-exchange-rates-query" :loading="loading" @search="search" /></template>
<template #notices><LoadErrorState v-if="error" title="月度汇率加载失败" :message="error.message" @retry="load" /></template>
<template #before-results><dl class="rate-summary density-detail-overview"><div v-for="item in [{label:'最新维护汇率',value:summary.latestUsdToCnyAverage??'—'},{label:'汇率月份',value:summary.latestMonth||'—'},{label:'最后更新人',value:summary.updatedBy||'—'},{label:'最后更新时间',value:summary.updatedAt||'—'}]" :key="item.label"><dt>{{ item.label }}</dt><dd>{{ item.value }}</dd></div></dl></template>
<template #toolbar-actions><el-button v-if="access.can(permission.exchangeRateCreate)" type="primary" @click="openCreate"><Plus :size="16" />更新月度汇率</el-button><el-button v-if="access.can(permission.exchangeRateExport) && access.can('platform:download:list')" :loading="exporting" @click="exportCsv"><Download :size="16" />导出</el-button></template>
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button data-tone="neutral" class="density-action" v-if="access.can(permission.exchangeRateEdit)" @click="openEdit(row as MonthlyExchangeRate)">编辑</el-button><el-button class="density-action" v-if="access.can(permission.exchangeRateStatus)" :data-tone="(row as MonthlyExchangeRate).status === 'ACTIVE' ? 'danger' : 'success'" @click="toggle(row as MonthlyExchangeRate)">{{ (row as MonthlyExchangeRate).status === 'ACTIVE' ? '停用' : '启用' }}</el-button></div></template>
<template #conversion="{ row }">1 USD = {{ (row as MonthlyExchangeRate).usdToCnyAverage }} CNY</template><template #status="{ row }"><el-tag :type="(row as MonthlyExchangeRate).status === 'ACTIVE' ? 'success' : 'info'">{{ (row as MonthlyExchangeRate).status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></template>
</DataTable></section>
<AppDialog class="admin-density-dialog" v-model="editorOpen" :title="editingMonth ? `编辑月度汇率 · ${editingMonth}` : '更新月度汇率'" width="min(560px, calc(100vw - 28px))" :confirm-loading="saving" @confirm="save">
<el-form class="density-form" label-position="left" label-width="160px" :disabled="saving">
<table class="density-table density-table--editable" aria-label="月度汇率资料"><tbody>
<tr><td><el-form-item label="汇率月份"><span v-if="editingMonth">{{ form.month }}</span><el-date-picker v-else v-model="form.month" type="month" value-format="YYYY-MM" /></el-form-item></td></tr>
<tr><td><el-form-item label="美元兑人民币平均汇率"><el-input-number v-model="form.usdToCnyAverage" :min="0.00000001" :precision="8" placeholder="请输入美元兑人民币月均汇率" /></el-form-item></td></tr>
<tr><td><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="1000" placeholder="请输入备注(选填)" /></el-form-item></td></tr>
</tbody></table>
<p class="density-dialog-note">汇率方向:1 USD = X CNY</p>
</el-form>
</AppDialog>
</ListPageLayout></AdminPage></template>
<style scoped>.table-panel{min-width:0;overflow:hidden}</style>