feat(energy): 实现能源账户管理页面(列表+Drawer+充值)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
103
apps/web-antd/src/views/energy/account/data.ts
Normal file
103
apps/web-antd/src/views/energy/account/data.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { EnergyAccountApi } from '#/api/energy/account';
|
||||
|
||||
import { useActionColumn } from '#/utils/table';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
export const ACCOUNT_STATUS_OPTIONS = [
|
||||
{ label: '正常', value: 1, color: 'green' },
|
||||
{ label: '冻结', value: 0, color: 'red' },
|
||||
];
|
||||
|
||||
export const FLOW_TYPE_OPTIONS = [
|
||||
{ label: '充值', value: 1, color: 'green' },
|
||||
{ label: '扣款', value: 2, color: 'red' },
|
||||
{ label: '退款', value: 3, color: 'blue' },
|
||||
{ label: '调整', value: 4, color: 'orange' },
|
||||
];
|
||||
|
||||
/** 搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'customerName',
|
||||
label: '客户名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入客户名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'accountStatus',
|
||||
label: '账户状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: ACCOUNT_STATUS_OPTIONS,
|
||||
placeholder: '请选择账户状态',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeTableGridOptions<EnergyAccountApi.Account>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'balance',
|
||||
title: '当前余额',
|
||||
minWidth: 120,
|
||||
align: 'right',
|
||||
slots: { default: 'balance' },
|
||||
},
|
||||
{
|
||||
field: 'initBalance',
|
||||
title: '初始余额',
|
||||
minWidth: 100,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
field: 'accumulatedRecharge',
|
||||
title: '累计充值',
|
||||
minWidth: 120,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
field: 'accumulatedConsume',
|
||||
title: '累计消费',
|
||||
minWidth: 120,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
field: 'reminderThreshold',
|
||||
title: '预警阈值',
|
||||
minWidth: 100,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
field: 'accountStatus',
|
||||
title: '状态',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
slots: { default: 'accountStatus' },
|
||||
},
|
||||
useActionColumn(3),
|
||||
];
|
||||
}
|
||||
130
apps/web-antd/src/views/energy/account/index.vue
Normal file
130
apps/web-antd/src/views/energy/account/index.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { EnergyAccountApi } from '#/api/energy/account';
|
||||
|
||||
import { ref, onMounted, h } from 'vue';
|
||||
import { Page, SummaryCard } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
import { message, Tag, Modal, InputNumber } from 'ant-design-vue';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getAccountPage, getAccountSummary, exportAccount, updateThreshold } from '#/api/energy/account';
|
||||
import { $t } from '#/locales';
|
||||
import { useGridColumns, useGridFormSchema, ACCOUNT_STATUS_OPTIONS } from './data';
|
||||
import AccountDrawer from './modules/account-drawer.vue';
|
||||
import RechargeModal from './modules/recharge-modal.vue';
|
||||
|
||||
const summary = ref<EnergyAccountApi.Summary>({ totalCount: 0, totalBalance: 0, totalRecharge: 0, warningCount: 0 });
|
||||
const drawerRef = ref();
|
||||
const rechargeRef = ref();
|
||||
|
||||
async function loadSummary() {
|
||||
summary.value = await getAccountSummary();
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
loadSummary();
|
||||
}
|
||||
|
||||
function handleDetail(row: EnergyAccountApi.Account) {
|
||||
drawerRef.value?.open(row);
|
||||
}
|
||||
|
||||
function handleRecharge(row: EnergyAccountApi.Account) {
|
||||
rechargeRef.value?.open(row);
|
||||
}
|
||||
|
||||
// Set threshold — simple modal with InputNumber
|
||||
function handleSetThreshold(row: EnergyAccountApi.Account) {
|
||||
let thresholdValue = row.reminderThreshold || 0;
|
||||
Modal.confirm({
|
||||
title: '设置预警阈值',
|
||||
content: () => {
|
||||
return h(InputNumber, {
|
||||
value: thresholdValue,
|
||||
min: 0,
|
||||
precision: 2,
|
||||
style: { width: '100%' },
|
||||
placeholder: '请输入预警阈值',
|
||||
'onUpdate:value': (val: number) => { thresholdValue = val; },
|
||||
});
|
||||
},
|
||||
async onOk() {
|
||||
await updateThreshold(row.id!, thresholdValue);
|
||||
message.success('阈值设置成功');
|
||||
handleRefresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
const data = await exportAccount(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '能源账户.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: { collapsed: true, schema: useGridFormSchema() },
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getAccountPage({ pageNo: page.currentPage, pageSize: page.pageSize, ...formValues });
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: { keyField: 'id', isHover: true },
|
||||
rowClassName: ({ row }: any) => (row.balance < 0 ? 'bg-red-50' : ''),
|
||||
toolbarConfig: { refresh: true, search: true },
|
||||
} as VxeTableGridOptions<EnergyAccountApi.Account>,
|
||||
});
|
||||
|
||||
onMounted(loadSummary);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- Summary Cards -->
|
||||
<div class="mb-4 grid grid-cols-4 gap-4 px-4 pt-4">
|
||||
<SummaryCard title="账户总数" :value="summary.totalCount" icon="lucide:users" icon-color="#1890ff" icon-bg-color="#e6f7ff" />
|
||||
<SummaryCard title="总余额" :value="summary.totalBalance" prefix="¥" :decimals="2" icon="lucide:wallet" icon-color="#52c41a" icon-bg-color="#f6ffed" />
|
||||
<SummaryCard title="累计充值" :value="summary.totalRecharge" prefix="¥" :decimals="2" icon="lucide:trending-up" icon-color="#fa8c16" icon-bg-color="#fff7e6" />
|
||||
<SummaryCard title="预警账户" :value="summary.warningCount" icon="lucide:alert-triangle" icon-color="#ff4d4f" icon-bg-color="#fff2f0" tooltip="余额低于阈值" />
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<Grid table-title="账户列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{ label: '导出', type: 'primary', icon: ACTION_ICON.DOWNLOAD, auth: ['energy:account:export'], onClick: handleExport },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #balance="{ row }">
|
||||
<span :class="row.balance < 0 ? 'font-bold text-red-500' : row.balance <= row.reminderThreshold ? 'font-bold text-orange-500' : 'font-bold text-green-500'">
|
||||
¥{{ row.balance?.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #accountStatus="{ row }">
|
||||
<Tag :color="ACCOUNT_STATUS_OPTIONS.find(o => o.value === row.accountStatus)?.color">
|
||||
{{ ACCOUNT_STATUS_OPTIONS.find(o => o.value === row.accountStatus)?.label }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{ label: '详情', type: 'link', icon: ACTION_ICON.VIEW, onClick: () => handleDetail(row) },
|
||||
{ label: '充值', type: 'link', auth: ['energy:account:update'], onClick: () => handleRecharge(row) },
|
||||
{ label: '设置阈值', type: 'link', auth: ['energy:account:update'], onClick: () => handleSetThreshold(row) },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<AccountDrawer ref="drawerRef" />
|
||||
<RechargeModal ref="rechargeRef" @success="handleRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { Drawer, Tabs } from 'ant-design-vue';
|
||||
import type { EnergyAccountApi } from '#/api/energy/account';
|
||||
import { getProjectAccountList, getFlowPage } from '#/api/energy/account';
|
||||
import { FLOW_TYPE_OPTIONS } from '../data';
|
||||
|
||||
const visible = ref(false);
|
||||
const account = ref<EnergyAccountApi.Account>();
|
||||
const projectAccounts = ref<EnergyAccountApi.ProjectAccount[]>([]);
|
||||
const flows = ref<EnergyAccountApi.Flow[]>([]);
|
||||
const activeTab = ref('project');
|
||||
|
||||
function open(data: EnergyAccountApi.Account) {
|
||||
account.value = data;
|
||||
visible.value = true;
|
||||
loadProjectAccounts();
|
||||
loadFlows();
|
||||
}
|
||||
|
||||
async function loadProjectAccounts() {
|
||||
if (!account.value?.id) return;
|
||||
projectAccounts.value = await getProjectAccountList(account.value.id);
|
||||
}
|
||||
|
||||
async function loadFlows() {
|
||||
if (!account.value?.id) return;
|
||||
const result = await getFlowPage({ pageNo: 1, pageSize: 50, accountId: account.value.id } as any);
|
||||
flows.value = result?.list || [];
|
||||
}
|
||||
|
||||
// Project account columns for a-table
|
||||
const projectColumns = [
|
||||
{ title: '项目名称', dataIndex: 'projectName', width: 150 },
|
||||
{ title: '合同编号', dataIndex: 'contractCode', width: 120 },
|
||||
{ title: '项目余额', dataIndex: 'balance', width: 100, align: 'right' as const },
|
||||
{ title: '已划账', dataIndex: 'accumulatedRecharge', width: 100, align: 'right' as const },
|
||||
{ title: '已消费', dataIndex: 'accumulatedConsume', width: 100, align: 'right' as const },
|
||||
];
|
||||
|
||||
// Flow columns for a-table
|
||||
const flowColumns = [
|
||||
{ title: '时间', dataIndex: 'createTime', width: 180 },
|
||||
{ title: '流水类型', dataIndex: 'flowType', width: 80, customRender: ({ text }: any) => FLOW_TYPE_OPTIONS.find(o => o.value === text)?.label || text },
|
||||
{ title: '变动金额', dataIndex: 'amount', width: 100, align: 'right' as const },
|
||||
{ title: '变动前余额', dataIndex: 'balanceBefore', width: 100, align: 'right' as const },
|
||||
{ title: '变动后余额', dataIndex: 'balanceAfter', width: 100, align: 'right' as const },
|
||||
{ title: '关联单据号', dataIndex: 'bizCode', width: 120 },
|
||||
{ title: '备注', dataIndex: 'remark', width: 150 },
|
||||
];
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer v-model:open="visible" :title="account?.customerName + ' - 账户详情'" width="720">
|
||||
<!-- Balance card -->
|
||||
<div class="mb-4 rounded-lg p-6 text-white" style="background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%)">
|
||||
<div class="text-sm opacity-80">当前余额</div>
|
||||
<div class="text-3xl font-bold">¥{{ account?.balance?.toFixed(2) }}</div>
|
||||
<div class="mt-2 flex gap-4 text-sm opacity-80">
|
||||
<span>累计充值 ¥{{ account?.accumulatedRecharge?.toFixed(2) }}</span>
|
||||
<span>累计消费 ¥{{ account?.accumulatedConsume?.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs v-model:activeKey="activeTab">
|
||||
<Tabs.TabPane key="project" tab="项目账户">
|
||||
<a-table :data-source="projectAccounts" :columns="projectColumns" :pagination="false" size="small" row-key="id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="flow" tab="账户流水">
|
||||
<a-table :data-source="flows" :columns="flowColumns" :pagination="false" size="small" row-key="id" :scroll="{ y: 400 }" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Modal, InputNumber, Input, Descriptions, message } from 'ant-design-vue';
|
||||
import { rechargeAccount } from '#/api/energy/account';
|
||||
import type { EnergyAccountApi } from '#/api/energy/account';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const account = ref<EnergyAccountApi.Account>();
|
||||
const amount = ref<number>(0);
|
||||
const remark = ref('');
|
||||
|
||||
const afterBalance = computed(() => (account.value?.balance || 0) + (amount.value || 0));
|
||||
|
||||
function open(data: EnergyAccountApi.Account) {
|
||||
account.value = data;
|
||||
amount.value = 0;
|
||||
remark.value = '';
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!amount.value || amount.value <= 0) { message.warning('请输入充值金额'); return; }
|
||||
loading.value = true;
|
||||
try {
|
||||
await rechargeAccount(account.value!.customerId!, amount.value, remark.value || undefined);
|
||||
message.success('充值成功');
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '充值失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:open="visible" title="账户充值" :confirm-loading="loading" @ok="handleConfirm">
|
||||
<Descriptions :column="1" bordered size="small" class="mb-4">
|
||||
<Descriptions.Item label="客户名称">{{ account?.customerName }}</Descriptions.Item>
|
||||
<Descriptions.Item label="当前余额">¥{{ account?.balance?.toFixed(2) }}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block font-medium">充值金额 <span class="text-red-500">*</span></label>
|
||||
<InputNumber v-model:value="amount" :min="0.01" :precision="2" placeholder="请输入充值金额" class="w-full" addon-after="元" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block font-medium">备注</label>
|
||||
<Input.TextArea v-model:value="remark" :rows="2" placeholder="请输入备注(选填)" />
|
||||
</div>
|
||||
<div class="rounded bg-blue-50 p-3 text-center">
|
||||
充值后余额:<strong class="text-blue-600">¥{{ afterBalance.toFixed(2) }}</strong>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user