75 lines
9.6 KiB
Vue
75 lines
9.6 KiB
Vue
<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>
|