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

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/content/cms_content');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/customer_service/cs_ticket');
</script>

View File

@@ -0,0 +1,41 @@
<template>
<a-space direction="vertical" fill :size="16">
<a-alert>统计报表仅展示后端当前提供的真实汇总数据</a-alert>
<a-card title="平台资源统计" :bordered="false">
<a-spin :loading="loading" style="width: 100%">
<a-grid :cols="{ xs: 1, sm: 2, md: 3, lg: 4 }" :col-gap="16" :row-gap="16">
<a-grid-item v-for="[key, value] in metrics" :key="key">
<a-statistic :title="labels[key] ?? key" :value="value" show-group-separator />
</a-grid-item>
</a-grid>
</a-spin>
</a-card>
</a-space>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import { computed, onMounted, ref } from 'vue';
import { platformApi } from '@/api/platform';
const loading = ref(false);
const overview = ref<Record<string, number>>({});
const labels: Record<string, string> = {
gas_basic_count: '气站数量',
delivery_basic_count: '配送站数量',
staff_account_count: '工作人员数量',
user_account_count: '用户数量',
};
const metrics = computed(() => Object.entries(overview.value));
onMounted(async () => {
loading.value = true;
try {
overview.value = await platformApi.overview();
} catch (error) {
Message.error((error as Error).message);
} finally {
loading.value = false;
}
});
</script>

View File

@@ -1 +0,0 @@
<template><CrudListPage title="配送点" resource="/delivery/delivery_basic" :fields="fields" /></template><script setup lang="ts">import CrudListPage from '@/views/shared/CrudListPage.vue'; const fields = [{ key: 'delivery_code', label: '配送点编码', required: true }, { key: 'name', label: '配送点名称', required: true }, { key: 'principal', label: '负责人' }, { key: 'address', label: '地址' }];</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/delivery/delivery_account');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/delivery/delivery_basic');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/delivery/delivery_task');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/delivery/delivery_track');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/delivery/delivery_track_point');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/device/dev_device_binding');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/device/dev_smart_cylinder_valve');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/device/dev_telemetry');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/ec/ec_cart');
</script>

View File

@@ -1,6 +0,0 @@
<template><TreePage :definition="definition" /></template>
<script setup lang="ts">
import TreePage from '@/views/shared/TreePage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/ec/ec_category');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/ec/ec_order');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/ec/ec_order_item');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/ec/ec_product');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/ec/ec_product_attribute');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/ec/ec_product_image');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/ec/ec_review');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/finance/fin_payment');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/finance/fin_reconciliation');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/finance/fin_settlement');
</script>

View File

@@ -1 +0,0 @@
<template><CrudListPage title="可燃气体站" resource="/gas/gas_basic" :fields="fields" /></template><script setup lang="ts">import CrudListPage from '@/views/shared/CrudListPage.vue'; const fields = [{ key: 'code', label: '站点编码', required: true }, { key: 'name', label: '站点名称', required: true }, { key: 'principal', label: '负责人' }, { key: 'address', label: '地址' }];</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/gas/gas_account');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/gas/gas_basic');
</script>

View File

@@ -15,28 +15,25 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import bannerImage from '@/assets/images/login-banner.png';
const { t } = useI18n();
const carouselItem = computed(() => [
const carouselItem = [
{
slogan: t('login.banner.slogan1'),
subSlogan: t('login.banner.subSlogan1'),
slogan: '统一运营',
subSlogan: '覆盖气站、配送站、人员、用户与产品',
image: bannerImage,
},
{
slogan: t('login.banner.slogan2'),
subSlogan: t('login.banner.subSlogan2'),
slogan: '气体配送闭环',
subSlogan: '合同、气瓶、订单、轨迹、确认与支付',
image: bannerImage,
},
{
slogan: t('login.banner.slogan3'),
subSlogan: t('login.banner.subSlogan3'),
slogan: '资金安全',
subSlogan: '统一钱包、流水、退款与提现审核',
image: bannerImage,
},
]);
];
</script>
<style lang="less" scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="login-form-wrapper">
<div class="login-form-title">{{ $t('login.form.title') }}</div>
<div class="login-form-sub-title">{{ $t('login.form.title') }}</div>
<div class="login-form-title">登录和气平台</div>
<div class="login-form-sub-title">使用平台管理员账户登录</div>
<div class="login-form-error-msg">{{ errorMessage }}</div>
<a-form
ref="loginForm"
@@ -12,13 +12,13 @@
>
<a-form-item
field="username"
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
:rules="[{ required: true, message: '请输入用户名' }]"
:validate-trigger="['change', 'blur']"
hide-label
>
<a-input
v-model="userInfo.username"
:placeholder="$t('login.form.userName.placeholder')"
placeholder="用户名"
>
<template #prefix>
<icon-user />
@@ -27,13 +27,13 @@
</a-form-item>
<a-form-item
field="password"
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
:rules="[{ required: true, message: '请输入密码' }]"
:validate-trigger="['change', 'blur']"
hide-label
>
<a-input-password
v-model="userInfo.password"
:placeholder="$t('login.form.password.placeholder')"
placeholder="密码"
allow-clear
>
<template #prefix>
@@ -48,15 +48,11 @@
:model-value="loginConfig.rememberPassword"
@change="setRememberPassword as any"
>
{{ $t('login.form.rememberPassword') }}
记住用户名
</a-checkbox>
<a-link>{{ $t('login.form.forgetPassword') }}</a-link>
</div>
<a-button type="primary" html-type="submit" long :loading="loading">
{{ $t('login.form.login') }}
</a-button>
<a-button type="text" long class="login-form-register-btn">
{{ $t('login.form.register') }}
登录
</a-button>
</a-space>
</a-form>
@@ -68,14 +64,12 @@ import { Message } from '@arco-design/web-vue';
import type { ValidatedError } from '@arco-design/web-vue/es/form/interface';
import { useStorage } from '@vueuse/core';
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import type { LoginData } from '@/api/auth';
import useLoading from '@/hooks/loading';
import { useUserStore } from '@/store';
const router = useRouter();
const { t } = useI18n();
const errorMessage = ref('');
const { loading, setLoading } = useLoading();
const userStore = useUserStore();
@@ -83,7 +77,7 @@ const userStore = useUserStore();
const loginConfig = useStorage('login-config', {
rememberPassword: true,
username: 'admin',
password: 'admin',
password: '',
});
const userInfo = reactive({
username: loginConfig.value.username,
@@ -104,17 +98,16 @@ const handleSubmit = async ({
await userStore.login(values as LoginData);
const { redirect, ...othersQuery } = router.currentRoute.value.query;
router.push({
name: (redirect as string) || 'DashboardOverview',
name: (redirect as string) || 'dashboard-overview',
query: {
...othersQuery,
},
});
Message.success(t('login.form.login.success'));
Message.success('登录成功');
const { rememberPassword } = loginConfig.value;
const { username, password } = values;
// 实际生产环境需要进行加密存储<E5AD98>? // The actual production environment requires encrypted storage.
loginConfig.value.username = rememberPassword ? username : '';
loginConfig.value.password = rememberPassword ? password : '';
loginConfig.value.password = '';
} catch (err) {
errorMessage.value = (err as Error).message;
} finally {

View File

@@ -1,11 +1,8 @@
<template>
<div class="container">
<div class="logo">
<img
alt="logo"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
/>
<div class="logo-text">Arco Design Pro</div>
<img alt="和气平台" src="@/assets/logo.svg" />
<div class="logo-text">和气平台总后台</div>
</div>
<LoginBanner />
<div class="content">

View File

@@ -1 +0,0 @@
<template><a-card title="平台账号" :bordered="false"><a-table :data="list" :loading="loading" :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="platform_role_code" /><a-table-column title="手机号" data-index="phone_masked" /><a-table-column title="状态" data-index="status" /></template></a-table></a-card></template><script setup lang="ts">import { Message } from '@arco-design/web-vue'; import { onMounted, ref } from 'vue'; import { platformApi } from '@/api/platform'; const list = ref<Record<string, unknown>[]>([]); const loading = ref(false); onMounted(async () => { loading.value = true; try { list.value = (await platformApi.listAccount()).list; } catch (error) { Message.error((error as Error).message); } finally { loading.value = false; } });</script>

View File

@@ -1,6 +0,0 @@
<template><TreePage :definition="definition" /></template>
<script setup lang="ts">
import TreePage from '@/views/shared/TreePage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/platform/platform_menu');
</script>

View File

@@ -1,6 +0,0 @@
<template><TreePage :definition="definition" /></template>
<script setup lang="ts">
import TreePage from '@/views/shared/TreePage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/platform/platform_menu');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/platform/platform_role');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/platform/platfrom_account');
</script>

View File

@@ -1 +0,0 @@
<template><CrudListPage title="平台角色" resource="/platform/platform_role" :fields="fields" /></template><script setup lang="ts">import CrudListPage from '@/views/shared/CrudListPage.vue'; const fields = [{ key: 'role_code', label: '角色编码', required: true }, { key: 'name', label: '角色名称', required: true }, { key: 'data_scope', label: '数据范围', required: true }];</script>

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>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/staff/account');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/staff/account');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/staff/credential');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/user/account');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/user/account');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/user/address');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/user/service_relation');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/wallet/wallet');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/wallet/wallet_ledger');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/wallet/wallet_recharge');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/wallet/wallet_withdrawal');
</script>