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