fix(admin): standardize resource initialization and lists

This commit is contained in:
2026-08-05 12:19:13 +08:00
parent d88e18bd09
commit 2162fe5e11
36 changed files with 419 additions and 267 deletions

View File

@@ -204,6 +204,28 @@ const fieldLabels: Record<string, string> = {
path: '路由',
menu_identities: '菜单权限',
status: '状态',
confirm_type: '确认方式',
delivery_basic_identity: '配送点唯一标识',
description: '说明',
ec_category_identity: '商品分类唯一标识',
ec_product_identity: '商品唯一标识',
gas_basic_identity: '气站唯一标识',
gasorder_contract_identity: '配送合同唯一标识',
gasorder_contract_product_identities: '合同气瓶唯一标识',
producer_account_identity: '生产商唯一标识',
product_info_identity: '智能气阀唯一标识',
product_type_identity: '智能气阀类型唯一标识',
proof_uri: '凭证地址',
recipient_name: '签收人姓名',
recipient_phone: '签收人电话',
staff_account_identity: '工作人员唯一标识',
subject_identity: '结算主体唯一标识',
user_account_identity: '用户唯一标识',
user_address_identity: '用户地址唯一标识',
wallet_bank_identity: '银行卡唯一标识',
wallet_basic_identity: '钱包唯一标识',
warehouse_identity: '库房唯一标识',
withdrawable: '计入可提现余额',
};
const numbers = new Set([
@@ -229,6 +251,8 @@ const textareas = new Set([
]);
function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
const label = fieldLabels[key];
if (!label) throw new Error(`资源字段缺少中文名称:${key}`);
const type: ResourceFieldType = key.endsWith('_identity')
? 'identity'
: money.has(key)
@@ -246,7 +270,7 @@ function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
: key === 'password'
? 'password'
: 'text';
return { key, label: fieldLabels[key] ?? key, type, ...options };
return { key, label, type, ...options };
}
function relation(key: string, resource: string, required = false): ResourceField {

View File

@@ -1,16 +0,0 @@
<template>
<a-layout-footer class="footer">和气平台总后台</a-layout-footer>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped>
.footer {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
color: var(--color-text-2);
text-align: center;
}
</style>

View File

@@ -157,5 +157,11 @@ export default defineComponent({
font-size: 18px;
}
}
.arco-menu-item.arco-menu-selected {
color: rgb(var(--primary-6));
background-color: var(--color-primary-light-1);
box-shadow: inset 3px 0 0 rgb(var(--primary-6));
font-weight: 600;
}
}
</style>

View File

@@ -37,7 +37,6 @@
<a-layout-content>
<PageLayout />
</a-layout-content>
<Footer v-if="footer" />
</a-layout>
</a-layout>
</a-layout>
@@ -47,7 +46,6 @@
<script lang="ts" setup>
import { computed, onMounted, provide, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Footer from '@/components/footer/index.vue';
import Menu from '@/components/menu/index.vue';
import NavBar from '@/components/navbar/index.vue';
import TabBar from '@/components/tab-bar/index.vue';
@@ -67,7 +65,6 @@ const navbarHeight = `60px`;
const navbar = computed(() => appStore.navbar);
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
const hideMenu = computed(() => appStore.hideMenu);
const footer = computed(() => appStore.footer);
const menuWidth = computed(() => {
return appStore.menuCollapse ? 48 : appStore.menuWidth;
});

View File

@@ -17,7 +17,6 @@
>
<template v-if="item.money" #prefix>¥</template>
</a-statistic>
<div class="metric-hint">{{ item.hint }}</div>
</a-card>
</a-grid-item>
</a-grid>
@@ -69,15 +68,15 @@ const updatedAtLabel = computed(() => updatedAt.value || (errorMessage.value ? '
const overview = ref<DashboardOverview>(emptyOverview());
const router = useRouter();
const userStore = useUserStore();
const cards: { key: CountKey; label: string; hint: string; money?: boolean }[] = [
{ key: 'gas_basic_count', label: '气站总数', hint: '正常运营气站总数' },
{ key: 'delivery_basic_count', label: '配送点总数', hint: '正常运营配送点总数' },
{ key: 'user_count', label: '客户总数', hint: '有效客户账户总数' },
{ key: 'product_count', label: '智能气阀', hint: '平台智能气阀总数' },
{ key: 'today_order_count', label: '今日订单', hint: '自然日新增' },
{ key: 'today_order_amount', label: '今日应付金额', hint: '订单口径', money: true },
{ key: 'pending_ticket_count', label: '待受理工单', hint: '客服待办' },
{ key: 'paid_amount', label: '累计实收金额', hint: '支付成功口径', money: true },
const cards: { key: CountKey; label: string; money?: boolean }[] = [
{ key: 'gas_basic_count', label: '气站总数' },
{ key: 'delivery_basic_count', label: '配送点总数' },
{ key: 'user_count', label: '客户总数' },
{ key: 'product_count', label: '智能气阀' },
{ key: 'today_order_count', label: '今日订单' },
{ key: 'today_order_amount', label: '今日应付金额', money: true },
{ key: 'pending_ticket_count', label: '待受理工单' },
{ key: 'paid_amount', label: '累计实收金额', money: true },
];
const actions = [
{ label: '新建气站', route: 'organization-gas-basic', menu: 'gas_basic', icon: IconPlus },
@@ -148,8 +147,7 @@ onMounted(loadOverview);
.dashboard-spin { width: 100%; }
.dashboard-alert { margin-bottom: 16px; }
.dashboard-meta { margin-bottom: 12px; color: var(--color-text-3); font-size: 12px; text-align: right; }
.metric-card { min-height: 132px; }
.metric-hint { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--color-border-2); color: var(--color-text-2); font-size: 13px; line-height: 20px; }
.metric-card { min-height: 112px; }
.section-card { margin-top: 16px; }
.quick-action { height: 52px; justify-content: flex-start; padding: 0 18px; }
</style>

View File

@@ -6,7 +6,6 @@
<main class="login-card" aria-label="和气平台总后台登录">
<img class="login-card-logo" :src="logoUrl" alt="和气" />
<LoginForm />
<div class="login-footer">© 2026 和气 · 安全连接每一程</div>
</main>
</div>
</template>
@@ -61,16 +60,6 @@ import LoginForm from './components/login-form.vue';
margin: 0 auto 14px;
}
.login-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid rgba(29, 44, 75, 0.08);
color: #8a94a6;
font-size: 12px;
line-height: 20px;
text-align: center;
}
@media (max-width: @screen-sm) {
.login-page {
padding: 82px 16px 20px;

View File

@@ -31,8 +31,9 @@
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="ID" data-index="id" :width="100" />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" ellipsis tooltip>
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :width="columnWidth(field)" ellipsis tooltip>
<template #cell="{ record }">
{{ displayFieldValue(field, record) }}
</template>
@@ -95,9 +96,11 @@
</div>
<a-table :data="accountList" :loading="accountLoading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="用户名" data-index="username" />
<a-table-column title="显示名称" data-index="display_name" />
<a-table-column title="角色编码" data-index="role_code" />
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column title="用户名" data-index="username" :width="160" />
<a-table-column title="显示名称" data-index="display_name" :width="180" />
<a-table-column title="角色编码" data-index="role_code" :width="140" />
<a-table-column title="状态" :width="90">
<template #cell="{ record }">
<a-tag :color="Number(record.status) === 1 ? 'green' : 'red'">
@@ -282,7 +285,7 @@ const loading = ref(false);
const saving = ref(false);
const actionSubmitting = ref(false);
const page = ref(Math.max(1, Number(route.query.page) || 1));
const pageSize = 20;
const pageSize = 50;
const total = ref(0);
const list = ref<Row[]>([]);
const accountCounts = ref<Record<string, number>>({});
@@ -292,7 +295,7 @@ const accountLoading = ref(false);
const accountList = ref<Row[]>([]);
const accountOwner = ref<Row>({});
const accountPage = ref(1);
const accountPageSize = 10;
const accountPageSize = 50;
const accountTotal = ref(0);
const accountFormVisible = ref(false);
const accountSaving = ref(false);
@@ -362,6 +365,7 @@ const formMode = computed<'create' | 'edit'>(() =>
const formFields = computed(() =>
props.definition.fields.filter(
(field) =>
field.key !== 'identity' &&
(formMode.value === 'create' || field.type !== 'password') &&
!(
formMode.value === 'edit' &&
@@ -375,6 +379,7 @@ const displayFields = computed(() =>
props.definition.fields
.filter(
(field) =>
field.key !== 'identity' &&
field.key !== 'password' &&
!(
props.definition.name === 'gas_basic' &&
@@ -1128,12 +1133,26 @@ watch(
function optionLabel(option: Row) {
return String(option.name ?? option.title ?? option.code ?? option.username ?? option.contract_no ?? option.order_no ?? option.identity);
}
function columnWidth(field: ResourceField) {
if (field.key === 'identity') return 280;
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'select') return 140;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) return 220;
return 180;
}
</script>
<style scoped lang="less">
.filters {
margin-bottom: 16px;
}
.workflow-alert,
.action-alert {
margin-bottom: 16px;

View File

@@ -7,8 +7,9 @@
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip />
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" :width="columnWidth(field)" ellipsis tooltip />
<a-table-column title="操作" :width="90"><template #cell="{ record }"><a-button size="mini" @click="openDetail(record)">详情</a-button></template></a-table-column>
</template>
</a-table>
@@ -35,13 +36,13 @@ import { Message } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
import type { DetailAction, ResourceUiDefinition } from '@/api/resources';
import type { DetailAction, ResourceField, ResourceUiDefinition } from '@/api/resources';
type Row = Record<string, unknown>;
const props = defineProps<{ definition: ResourceUiDefinition }>();
const loading = ref(false);
const page = ref(1);
const pageSize = 20;
const pageSize = 50;
const total = ref(0);
const list = ref<Row[]>([]);
const filters = reactive({ keyword: '' });
@@ -51,8 +52,20 @@ const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const actionForm = reactive<Record<string, any>>({});
const displayFields = computed(() =>
props.definition.fields.filter((field) => field.key !== 'status'),
props.definition.fields.filter((field) => field.key !== 'identity' && field.key !== 'status'),
);
function columnWidth(field: ResourceField) {
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'select') return 140;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) return 220;
return 180;
}
const detailEntries = computed(() =>
Object.entries(detail.value).filter(
([key]) => key !== 'id' && !key.endsWith('_id'),