2026-08-19 16:45:51 +08:00
< script setup lang = "ts" >
2026-10-08 17:08:03 +08:00
import { useExportTask } from '@/modules/downloads'
2026-09-15 09:58:15 +08:00
import '@/styles/adminDensity.css'
2026-08-20 18:02:52 +08:00
import { onMounted , reactive , ref , watch } from 'vue'
2026-09-15 09:58:15 +08:00
import { Download , Plus } from '@lucide/vue'
2026-08-20 18:02:52 +08:00
import { ElMessage , ElMessageBox } from 'element-plus'
2026-08-19 16:45:51 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-20 18:02:52 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-19 16:45:51 +08:00
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'
2026-08-20 18:02:52 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-10-08 17:08:03 +08:00
import { changeExchangeRateStatus , createExchangeRate , getExchangeRate , getExchangeRateSummary , listExchangeRates , updateExchangeRate } from '../api/overseasMasterData'
2026-08-20 18:02:52 +08:00
import { overseasMasterDataPermissions as permission } from '../permissions'
import { masterDataErrorMessage } from '../apiError'
import type { MasterDataStatus , MonthlyExchangeRate } from '../types'
2026-08-19 16:45:51 +08:00
2026-10-08 17:08:03 +08:00
const submitExport = useExportTask ()
2026-08-20 18:02:52 +08:00
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 : '' })
2026-08-19 16:45:51 +08:00
const fields : QueryField [] = [
{ key : 'month' , type : 'date' , label : '查询月份' , placeholder : '请选择月份' , width : 200 , props : { type : 'month' , 'value-format' : 'YYYY-MM' } },
2026-08-20 18:02:52 +08:00
{ key : 'status' , type : 'select' , label : '状态' , placeholder : '全部状态' , width : 130 , options : [{ label : '启用' , value : 'ACTIVE' }, { label : '停用' , value : 'INACTIVE' }] },
2026-08-19 16:45:51 +08:00
]
const columns : DataTableColumn [] = [
2026-08-20 18:02:52 +08:00
{ 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 },
2026-08-19 16:45:51 +08:00
]
2026-08-20 18:02:52 +08:00
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 } }
2026-10-20 10:11:31 +08:00
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 , '汇率状态更新失败' )) } }
2026-10-08 17:08:03 +08:00
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 } }
2026-08-20 18:02:52 +08:00
watch ([ page , pageSize ], load ); onMounted ( load )
2026-08-19 16:45:51 +08:00
</ script >
2026-09-15 09:58:15 +08:00
< template >< AdminPage class = "admin-density" >< ListPageLayout aria -label =" 汇率管理 " >
2026-09-30 14:01:58 +08:00
< 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 >
2026-09-15 09:58:15 +08:00
2026-10-08 17:08:03 +08:00
< 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 >
2026-09-30 14:01:58 +08:00
< 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 >
2026-08-20 18:02:52 +08:00
< 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 >
2026-10-20 10:11:31 +08:00
< 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 >
2026-08-20 18:02:52 +08:00
</ ListPageLayout ></ AdminPage ></ template >
2026-09-15 09:58:15 +08:00
< style scoped >. table - panel { min - width : 0 ; overflow : hidden }</ style >