refactor platform admin from backend contract

This commit is contained in:
david
2026-07-28 11:08:47 +08:00
parent 899fc6186b
commit dc206f63fa
99 changed files with 755 additions and 2660 deletions

View File

@@ -12,6 +12,19 @@
</a-form-item>
<a-button type="primary" html-type="submit">查询</a-button>
</a-form>
<a-form v-if="definition.name === 'wallet_basic'" :model="walletOwner" layout="inline" class="wallet-owner" @submit.prevent="getOwnerWallet">
<a-form-item label="归属类型">
<a-select v-model="walletOwner.type" style="width: 140px">
<a-option value="user">用户</a-option>
<a-option value="staff">工作人员</a-option>
<a-option value="delivery">配送站</a-option>
<a-option value="gas">气站</a-option>
<a-option value="platform">平台</a-option>
</a-select>
</a-form-item>
<a-form-item label="归属标识"><a-input v-model="walletOwner.identity" placeholder="请输入 identity" /></a-form-item>
<a-button type="primary" html-type="submit">获取或创建钱包</a-button>
</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 />
@@ -39,7 +52,15 @@
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
<a-textarea v-else-if="field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
<a-select v-else-if="field.type === 'role-code'" v-model="form[field.key]">
<a-select v-else-if="field.type === 'select'" v-model="form[field.key]" allow-clear>
<a-option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</a-option>
</a-select>
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="form[field.key]" :multiple="field.type === 'identity-list'" allow-clear allow-search>
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">
{{ optionLabel(option) }}
</a-option>
</a-select>
<a-select v-else-if="field.key === 'platform_role_code'" v-model="form[field.key]">
<a-option v-for="role in roleOptions" :key="role.role_code" :value="role.role_code">{{ role.name }}</a-option>
</a-select>
<a-input v-else v-model="form[field.key]" :placeholder="`请输入${field.label}`" />
@@ -49,21 +70,28 @@
<a-drawer :visible="detailVisible" title="详情" :width="540" @cancel="detailVisible = false">
<a-descriptions :column="1" bordered>
<a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="key">{{ value ?? '-' }}</a-descriptions-item>
<a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="key">
<pre v-if="typeof value === 'object'" class="json-value">{{ formatValue(value) }}</pre>
<template v-else>{{ value ?? '-' }}</template>
</a-descriptions-item>
</a-descriptions>
<a-space class="detail-actions">
<a-button v-for="action in definition.detailActions" :key="action.name" type="primary" @click="openDetailAction(action)">{{ action.name }}</a-button>
<a-button v-for="action in definition.detailActions" :key="action.name" type="primary" :status="action.danger ? 'danger' : 'normal'" @click="openDetailAction(action)">{{ action.name }}</a-button>
</a-space>
</a-drawer>
<a-modal :visible="actionVisible" :title="activeAction?.name" @cancel="actionVisible = false" @ok="submitDetailAction">
<a-form :model="actionForm" layout="vertical">
<a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required">
<a-form-item v-for="field in activeAction?.fields ?? []" :key="field.key" :label="field.label" :required="field.required">
<a-input-number v-if="field.type === 'number'" v-model="actionForm[field.key]" />
<a-switch v-else-if="field.type === 'boolean'" v-model="actionForm[field.key]" />
<a-textarea v-else-if="field.type === 'textarea'" v-model="actionForm[field.key]" />
<a-select v-else-if="field.type === 'menu-identities'" v-model="actionForm[field.key]" multiple allow-search>
<a-option v-for="menu in menuOptions" :key="menu.identity" :value="menu.identity">{{ menu.name }}</a-option>
<a-date-picker v-else-if="field.type === 'datetime'" v-model="actionForm[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
<a-select v-else-if="field.type === 'select'" v-model="actionForm[field.key]">
<a-option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</a-option>
</a-select>
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="actionForm[field.key]" :multiple="field.type === 'identity-list'" allow-search>
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">{{ optionLabel(option) }}</a-option>
</a-select>
<a-input v-else v-model="actionForm[field.key]" />
</a-form-item>
@@ -75,7 +103,7 @@
import { Message, Modal } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import { platformApi, type PlatformMenu, type PlatformRole } from '@/api/platform';
import { platformApi, type PlatformRole } from '@/api/platform';
import {
buildResourcePayload,
isMissingField,
@@ -91,6 +119,7 @@ const pageSize = 20;
const total = ref(0);
const list = ref<Row[]>([]);
const filters = reactive({ keyword: '' });
const walletOwner = reactive({ type: 'user', identity: '' });
const formVisible = ref(false);
const detailVisible = ref(false);
const editingIdentity = ref('');
@@ -99,10 +128,10 @@ const detail = ref<Row>({});
const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const actionForm = reactive<Record<string, any>>({});
const menuOptions = ref<PlatformMenu[]>([]);
const roleOptions = ref<PlatformRole[]>([]);
const canCreate = computed(() => props.definition.mode !== 'readonly');
const canEdit = computed(() => props.definition.mode === 'writable');
const relationOptions = reactive<Record<string, Row[]>>({});
const canCreate = computed(() => ['writable', 'append_only'].includes(props.definition.mode) || ['product_info', 'gasorder_contract', 'gasorder_basic'].includes(props.definition.name));
const canEdit = computed(() => props.definition.mode === 'writable' || ['product_info', 'gasorder_contract'].includes(props.definition.name));
const canArchive = computed(() => props.definition.mode === 'writable');
const formMode = computed<'create' | 'edit'>(() =>
editingIdentity.value ? 'edit' : 'create',
@@ -120,6 +149,14 @@ const detailEntries = computed(() =>
([key]) => key !== 'id' && !key.endsWith('_id'),
),
);
const currentIdentity = computed(() =>
String(
detail.value.identity ??
(detail.value.order as Row | undefined)?.identity ??
(detail.value.contract as Row | undefined)?.identity ??
'',
),
);
function resetForm(data?: Row) {
for (const field of props.definition.fields) {
@@ -172,15 +209,15 @@ async function openDetail(row: Row) {
async function openDetailAction(action: DetailAction) {
activeAction.value = action;
for (const field of action.fields) actionForm[field.key] = undefined;
if (action.fields.some((field) => field.type === 'menu-identities')) {
for (const field of action.fields ?? []) actionForm[field.key] = undefined;
if (action.resource.includes('/menu')) {
try {
const identity = String(detail.value.identity);
const identity = currentIdentity.value;
const [menus, assigned] = await Promise.all([
platformApi.listMenu(),
platformApi.listRoleMenuIdentities(identity),
]);
menuOptions.value = menus.list;
relationOptions['/platform_menu'] = menus.list;
actionForm.menu_identities = assigned.menu_identities;
} catch (error) {
Message.error((error as Error).message);
@@ -194,7 +231,7 @@ async function submitDetailAction() {
const action = activeAction.value;
if (!action) return;
if (
action.fields.some(
(action.fields ?? []).some(
(field) => field.required && isMissingField(actionForm[field.key]),
)
) {
@@ -203,19 +240,16 @@ async function submitDetailAction() {
}
try {
const payload = {
...action.payload,
...buildResourcePayload(action.fields, actionForm),
...buildResourcePayload(action.fields ?? [], actionForm),
};
if (action.fields.some((field) => field.type === 'menu-identities')) {
if (action.resource.includes('/menu')) {
await platformApi.replaceRoleMenus(
String(detail.value.identity),
currentIdentity.value,
payload.menu_identities as string[],
);
} else {
await resourceApi.create(
action.resource.replace(':identity', String(detail.value.identity)),
payload,
);
const path = action.resource.replace(':identity', currentIdentity.value);
await resourceApi.action(path, action.method ?? 'POST', payload);
}
Message.success('操作成功');
actionVisible.value = false;
@@ -281,15 +315,51 @@ async function changePage(next: number) {
await load();
}
async function getOwnerWallet() {
if (!walletOwner.identity.trim()) {
Message.warning('请输入归属标识');
return;
}
try {
detail.value = await resourceApi.detail<Row>(
'/wallet_basic/owner',
`${walletOwner.type}/${walletOwner.identity.trim()}`,
);
detailVisible.value = true;
await load();
} catch (error) {
Message.error((error as Error).message);
}
}
function formatValue(value: unknown) {
return JSON.stringify(value, null, 2);
}
onMounted(async () => {
await load();
if (props.definition.fields.some((field) => field.type === 'role-code')) {
const actionFields = props.definition.detailActions?.flatMap((item) => item.fields ?? []) ?? [];
const relationPaths = new Set(
[...props.definition.fields, ...actionFields]
.map((field) => field.relation)
.filter((value): value is string => Boolean(value)),
);
await Promise.all(
[...relationPaths].map(async (resource) => {
relationOptions[resource] = (await resourceApi.list<Row>(resource, 1, 200)).list;
}),
);
if (props.definition.fields.some((field) => field.key === 'platform_role_code')) {
const result = await platformApi.listRole();
roleOptions.value = result.list.filter(
(role) => !role.is_system && role.status === 'enabled',
);
}
});
function optionLabel(option: Row) {
return String(option.name ?? option.title ?? option.code ?? option.username ?? option.contract_no ?? option.order_no ?? option.identity);
}
</script>
<style scoped lang="less">
@@ -304,4 +374,15 @@ onMounted(async () => {
.detail-actions {
margin-top: 16px;
}
.wallet-owner {
margin-bottom: 16px;
padding: 12px;
background: var(--color-fill-1);
}
.json-value {
max-height: 260px;
margin: 0;
overflow: auto;
white-space: pre-wrap;
}
</style>

View File

@@ -0,0 +1,15 @@
<template>
<TreePage v-if="definition.pageKind === 'tree'" :definition="definition" />
<CrudListPage v-else :definition="definition" />
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { getResource } from '@/api/resources';
import CrudListPage from './CrudListPage.vue';
import TreePage from './TreePage.vue';
const route = useRoute();
const definition = computed(() => getResource(String(route.meta.resource)));
</script>