refactor platform admin from backend contract
This commit is contained in:
@@ -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>
|
||||
|
||||
15
frontend/platform_admin/src/views/shared/ResourcePage.vue
Normal file
15
frontend/platform_admin/src/views/shared/ResourcePage.vue
Normal 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>
|
||||
Reference in New Issue
Block a user