mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-14 21:07:40 +00:00
refactor: 大规模代码整理与功能优化
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码 2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑 3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件 4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
<template>
|
||||
<div class="fa-full-height">
|
||||
<ElTabs v-model="activeTab" @tab-change="onTabChange">
|
||||
<!-- 平台端:全部发票 -->
|
||||
<ElTabPane v-if="isSuperAdmin" label="发票管理" name="platform">
|
||||
<FaSearchBar
|
||||
v-show="platformShowSearchBar"
|
||||
ref="platformSearchBarRef"
|
||||
v-model="platformSearchForm"
|
||||
:items="platformSearchItems"
|
||||
:rules="{}"
|
||||
:is-expand="false"
|
||||
:show-expand="true"
|
||||
:show-reset="true"
|
||||
:show-search="true"
|
||||
:disabled-search="false"
|
||||
:default-expanded="false"
|
||||
:button-left-limit="0"
|
||||
@search="handlePlatformSearch"
|
||||
@reset="handlePlatformReset"
|
||||
/>
|
||||
|
||||
<ElCard
|
||||
shadow="hover"
|
||||
class="fa-table-card"
|
||||
:style="{ 'margin-top': platformShowSearchBar ? '12px' : '0' }"
|
||||
>
|
||||
<FaTableHeader
|
||||
v-model:columns="platformColumnChecks"
|
||||
v-model:showSearchBar="platformShowSearchBar"
|
||||
:loading="platformLoading"
|
||||
@refresh="getPlatformData"
|
||||
/>
|
||||
|
||||
<FaTable
|
||||
ref="platformTableRef"
|
||||
:loading="platformLoading"
|
||||
:data="platformData"
|
||||
:columns="platformColumns"
|
||||
:pagination="platformPagination"
|
||||
@pagination:size-change="handlePlatformSizeChange"
|
||||
@pagination:current-change="handlePlatformCurrentChange"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 租户端:我的发票 -->
|
||||
<ElTabPane label="我的发票" name="my">
|
||||
<ElCard shadow="hover" class="fa-table-card" :style="{ 'margin-top': '0' }">
|
||||
<FaTableHeader :loading="myLoading" @refresh="getMyData">
|
||||
<template #left>
|
||||
<FaTableHeaderLeft perm-create="tenant:admin" @add="openApplyDialog" />
|
||||
</template>
|
||||
</FaTableHeader>
|
||||
|
||||
<FaTable
|
||||
ref="myTableRef"
|
||||
:loading="myLoading"
|
||||
:data="myData"
|
||||
:columns="myColumns"
|
||||
:pagination="myPagination"
|
||||
@pagination:size-change="handleMySizeChange"
|
||||
@pagination:current-change="handleMyCurrentChange"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
|
||||
<!-- 申请开票弹窗 -->
|
||||
<FaDialog v-model="applyDialogVisible" title="申请开票" width="520px">
|
||||
<FaForm
|
||||
ref="applyFormRef"
|
||||
v-model="applyFormData"
|
||||
:items="applyFormItems"
|
||||
:rules="applyRules"
|
||||
:show-footer="false"
|
||||
/>
|
||||
<template #footer>
|
||||
<ElButton @click="applyDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="applySubmitting" @click="submitApply">提交申请</ElButton>
|
||||
</template>
|
||||
</FaDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, h } from "vue";
|
||||
import { ElMessageBox, ElButton, ElTabs, ElTabPane, ElTag } from "element-plus";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import InvoiceAPI from "@/api/module_platform/invoice";
|
||||
import type { InvoiceTable } from "@/api/module_platform/invoice";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
|
||||
defineOptions({ name: "Invoice" });
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
const activeTab = ref("my");
|
||||
const isSuperAdmin = ref(true);
|
||||
const platformShowSearchBar = ref(true);
|
||||
|
||||
// ══════════════════ 标签函数 ════════════════════
|
||||
|
||||
function invoiceStatusLabel(status: number): string {
|
||||
const map: Record<number, string> = { 0: "待开具", 1: "已开具", 2: "开票失败", 3: "已作废" };
|
||||
return map[status] || String(status);
|
||||
}
|
||||
|
||||
function invoiceStatusType(status: number): string {
|
||||
const map: Record<number, string> = { 0: "warning", 1: "success", 2: "danger", 3: "info" };
|
||||
return map[status] || "info";
|
||||
}
|
||||
|
||||
// ══════════════════ 平台端:全部发票 ════════════════════
|
||||
|
||||
function buildPlatformRowActions(row: InvoiceTable): TableOperationAction[] {
|
||||
const actions: TableOperationAction[] = [];
|
||||
if (row.status === 0) {
|
||||
actions.push({
|
||||
key: "issue",
|
||||
label: "开具",
|
||||
artType: "edit",
|
||||
perm: "module_platform:invoice:update",
|
||||
run: () => issueInvoice(row),
|
||||
});
|
||||
}
|
||||
if (row.status === 1) {
|
||||
actions.push({
|
||||
key: "void",
|
||||
label: "作废",
|
||||
artType: "delete",
|
||||
perm: "module_platform:invoice:update",
|
||||
run: () => voidInvoice(row),
|
||||
});
|
||||
}
|
||||
return actions.filter((a) => a.perm != null && hasAuth(String(a.perm)));
|
||||
}
|
||||
|
||||
function formatPlatformOpCell(row: InvoiceTable) {
|
||||
const actions = buildPlatformRowActions(row);
|
||||
if (actions.length === 0) return "—";
|
||||
return renderTableOperationCell(actions, {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1",
|
||||
});
|
||||
}
|
||||
|
||||
const platformSearchForm = ref<Record<string, unknown>>({ tenant_id: "", status: undefined });
|
||||
|
||||
const platformSearchItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: "租户ID",
|
||||
key: "tenant_id",
|
||||
type: "input",
|
||||
placeholder: "租户ID",
|
||||
span: 6,
|
||||
props: { clearable: true },
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
key: "status",
|
||||
type: "select",
|
||||
placeholder: "状态",
|
||||
span: 6,
|
||||
props: {
|
||||
clearable: true,
|
||||
options: [
|
||||
{ label: "待开具", value: 0 },
|
||||
{ label: "已开具", value: 1 },
|
||||
{ label: "已作废", value: 2 },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const platformSearchBarRef = ref();
|
||||
|
||||
const {
|
||||
columns: platformColumns,
|
||||
columnChecks: platformColumnChecks,
|
||||
data: platformData,
|
||||
loading: platformLoading,
|
||||
pagination: platformPagination,
|
||||
getData: getPlatformData,
|
||||
replaceSearchParams: replacePlatformSearchParams,
|
||||
resetSearchParams: resetPlatformSearchParams,
|
||||
handleSizeChange: handlePlatformSizeChange,
|
||||
handleCurrentChange: handlePlatformCurrentChange,
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: InvoiceAPI.listInvoices,
|
||||
apiParams: {
|
||||
page_no: 1,
|
||||
page_size: 50,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<InvoiceTable>[] => [
|
||||
{ prop: "id", label: "ID", width: 60 },
|
||||
{ prop: "tenant_id", label: "租户ID", width: 80 },
|
||||
{ prop: "invoice_no", label: "发票号", width: 180, showOverflowTooltip: true },
|
||||
{ prop: "title", label: "抬头", minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: "invoice_type",
|
||||
label: "类型",
|
||||
width: 110,
|
||||
formatter: (row) => row.invoice_type || "—",
|
||||
},
|
||||
{
|
||||
prop: "amount",
|
||||
label: "金额",
|
||||
width: 120,
|
||||
formatter: (row) => `¥${((row.amount || 0) / 100).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
const t = invoiceStatusType(row.status) as "warning" | "success" | "info" | "danger";
|
||||
return h(ElTag, { type: t, size: "small" as const }, () =>
|
||||
invoiceStatusLabel(row.status)
|
||||
);
|
||||
},
|
||||
},
|
||||
{ prop: "address_info", label: "地址电话", width: 180, showOverflowTooltip: true },
|
||||
{ prop: "created_time", label: "申请时间", width: 160, showOverflowTooltip: true },
|
||||
{
|
||||
label: "操作",
|
||||
width: 150,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
formatter: (row) => formatPlatformOpCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
function handlePlatformSearch() {
|
||||
const params: Record<string, unknown> = {};
|
||||
if (platformSearchForm.value.status !== undefined && platformSearchForm.value.status !== "") {
|
||||
params.status = platformSearchForm.value.status;
|
||||
}
|
||||
const tid = parseInt(String(platformSearchForm.value.tenant_id || ""));
|
||||
if (!isNaN(tid)) params.tenant_id = tid;
|
||||
replacePlatformSearchParams(params);
|
||||
}
|
||||
|
||||
function handlePlatformReset() {
|
||||
platformSearchForm.value = { tenant_id: "", status: undefined };
|
||||
resetPlatformSearchParams();
|
||||
}
|
||||
|
||||
async function issueInvoice(row: InvoiceTable) {
|
||||
try {
|
||||
await InvoiceAPI.issueInvoice(row.id, {});
|
||||
getPlatformData();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function voidInvoice(row: InvoiceTable) {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt("请输入作废原因", "作废发票", { type: "warning" });
|
||||
await InvoiceAPI.voidInvoice(row.id, value);
|
||||
getPlatformData();
|
||||
} catch {
|
||||
/* cancelled */
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════ 租户端:我的发票 ════════════════════
|
||||
|
||||
const {
|
||||
columns: myColumns,
|
||||
data: myData,
|
||||
loading: myLoading,
|
||||
pagination: myPagination,
|
||||
getData: getMyData,
|
||||
handleSizeChange: handleMySizeChange,
|
||||
handleCurrentChange: handleMyCurrentChange,
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: InvoiceAPI.tenantListInvoices,
|
||||
apiParams: {
|
||||
page_no: 1,
|
||||
page_size: 50,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<InvoiceTable>[] => [
|
||||
{ prop: "id", label: "ID", width: 60 },
|
||||
{ prop: "invoice_no", label: "发票号", width: 180, showOverflowTooltip: true },
|
||||
{ prop: "title", label: "抬头", minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: "invoice_type",
|
||||
label: "类型",
|
||||
width: 110,
|
||||
formatter: (row) => row.invoice_type || "—",
|
||||
},
|
||||
{
|
||||
prop: "amount",
|
||||
label: "金额",
|
||||
width: 120,
|
||||
formatter: (row) => `¥${((row.amount || 0) / 100).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
const t = invoiceStatusType(row.status) as "warning" | "success" | "info" | "danger";
|
||||
return h(ElTag, { type: t, size: "small" as const }, () =>
|
||||
invoiceStatusLabel(row.status)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "created_time",
|
||||
label: "申请时间",
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row) => row.created_time || "—",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// ══════════════════ 申请开票弹窗 ════════════════════
|
||||
|
||||
const applyDialogVisible = ref(false);
|
||||
const applySubmitting = ref(false);
|
||||
const applyFormRef = ref();
|
||||
let applyFormData = reactive({
|
||||
order_id: null as number | null,
|
||||
title: "",
|
||||
invoice_type: "vat_normal",
|
||||
tax_no: "",
|
||||
address_info: "",
|
||||
bank_info: "",
|
||||
});
|
||||
|
||||
const applyFormItems: FormItem[] = [
|
||||
{
|
||||
label: "关联订单ID",
|
||||
key: "order_id",
|
||||
type: "inputNumber",
|
||||
span: 24,
|
||||
props: { min: 1, placeholder: "输入已支付订单ID" },
|
||||
},
|
||||
{
|
||||
label: "发票抬头",
|
||||
key: "title",
|
||||
type: "input",
|
||||
span: 24,
|
||||
props: { placeholder: "公司全称或个人姓名", maxlength: 100 },
|
||||
},
|
||||
{
|
||||
label: "发票类型",
|
||||
key: "invoice_type",
|
||||
type: "select",
|
||||
span: 24,
|
||||
props: {
|
||||
options: [
|
||||
{ label: "普通发票", value: "vat_normal" },
|
||||
{ label: "增值税专用发票", value: "vat_special" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "税号",
|
||||
key: "tax_no",
|
||||
type: "input",
|
||||
span: 24,
|
||||
props: { placeholder: "统一社会信用代码(可选)", maxlength: 30 },
|
||||
},
|
||||
{
|
||||
label: "银行信息",
|
||||
key: "bank_info",
|
||||
type: "input",
|
||||
span: 24,
|
||||
props: { placeholder: "开户行及账号(可选)", maxlength: 100 },
|
||||
},
|
||||
{
|
||||
label: "地址电话",
|
||||
key: "address_info",
|
||||
type: "input",
|
||||
span: 24,
|
||||
props: { placeholder: "注册地址及电话(专票必填)", maxlength: 200 },
|
||||
},
|
||||
];
|
||||
|
||||
const applyRules = {
|
||||
order_id: [{ required: true, message: "请输入订单ID", trigger: "blur" }],
|
||||
title: [{ required: true, message: "请输入发票抬头", trigger: "blur" }],
|
||||
};
|
||||
|
||||
function openApplyDialog() {
|
||||
Object.assign(applyFormData, {
|
||||
order_id: null,
|
||||
title: "",
|
||||
invoice_type: "vat_normal",
|
||||
tax_no: "",
|
||||
address_info: "",
|
||||
bank_info: "",
|
||||
});
|
||||
applyFormRef.value?.resetFields?.();
|
||||
applyDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitApply() {
|
||||
const form: any = applyFormRef.value;
|
||||
if (!form) return;
|
||||
const valid = await form.validate().catch(() => false);
|
||||
if (!valid) return;
|
||||
applySubmitting.value = true;
|
||||
try {
|
||||
await InvoiceAPI.applyInvoice({
|
||||
order_id: applyFormData.order_id!,
|
||||
title: applyFormData.title,
|
||||
invoice_type: applyFormData.invoice_type,
|
||||
tax_no: applyFormData.tax_no || undefined,
|
||||
address_info: applyFormData.address_info || undefined,
|
||||
bank_info: applyFormData.bank_info || undefined,
|
||||
});
|
||||
applyDialogVisible.value = false;
|
||||
getMyData();
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
applySubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════ Tab 切换 ════════════════════
|
||||
|
||||
function onTabChange(tab: string | number) {
|
||||
if (tab === "platform") getPlatformData();
|
||||
else if (tab === "my") getMyData();
|
||||
}
|
||||
|
||||
// ══════════════════ 初始化 ════════════════════
|
||||
|
||||
getMyData();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-tabs) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:deep(.el-tab-pane) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user