feat: 完善平台总后台模块
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<template><div class="dashboard"><a-grid :cols="{ xs: 1, sm: 2, lg: 5 }" :col-gap="16" :row-gap="16"><a-grid-item v-for="item in cards" :key="item.key"><a-card :bordered="false"><a-statistic :title="item.label" :value="overview[item.key] || 0" /></a-card></a-grid-item></a-grid><a-card title="运营概览" :bordered="false" class="intro"><p>跨组织查看可燃气体站、配送点、服务人员、业主客户与待处置安全事件。</p></a-card></div></template>
|
||||
<script setup lang="ts">import { Message } from '@arco-design/web-vue'; import { onMounted, ref } from 'vue'; import { platformApi } from '@/api/platform'; const overview = ref<Record<string, number>>({}); const cards = [{ key: 'gas_basic_count', label: '可燃气体站' }, { key: 'delivery_basic_count', label: '配送点' }, { key: 'staff_count', label: '在岗服务人员' }, { key: 'user_count', label: '业主客户' }, { key: 'pending_safety_count', label: '待处理安全事件' }]; onMounted(async () => { try { overview.value = await platformApi.overview(); } catch (error) { Message.error((error as Error).message); } });</script>
|
||||
<style scoped lang="less">.dashboard { padding: 20px; } .intro { margin-top: 16px; }</style>
|
||||
@@ -0,0 +1 @@
|
||||
<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
frontend/platform_admin/src/views/gas/basic/ListPage.vue
Normal file
1
frontend/platform_admin/src/views/gas/basic/ListPage.vue
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="banner">
|
||||
<div class="banner-inner">
|
||||
<a-carousel class="carousel" animation-name="fade">
|
||||
<a-carousel-item v-for="item in carouselItem" :key="item.slogan">
|
||||
<div :key="item.slogan" class="carousel-item">
|
||||
<div class="carousel-title">{{ item.slogan }}</div>
|
||||
<div class="carousel-sub-title">{{ item.subSlogan }}</div>
|
||||
<img class="carousel-image" :src="item.image" />
|
||||
</div>
|
||||
</a-carousel-item>
|
||||
</a-carousel>
|
||||
</div>
|
||||
</div>
|
||||
</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(() => [
|
||||
{
|
||||
slogan: t('login.banner.slogan1'),
|
||||
subSlogan: t('login.banner.subSlogan1'),
|
||||
image: bannerImage,
|
||||
},
|
||||
{
|
||||
slogan: t('login.banner.slogan2'),
|
||||
subSlogan: t('login.banner.subSlogan2'),
|
||||
image: bannerImage,
|
||||
},
|
||||
{
|
||||
slogan: t('login.banner.slogan3'),
|
||||
subSlogan: t('login.banner.subSlogan3'),
|
||||
image: bannerImage,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-inner {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel {
|
||||
height: 100%;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: var(--color-fill-1);
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
&-sub-title {
|
||||
margin-top: 8px;
|
||||
color: var(--color-text-3);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&-image {
|
||||
width: 320px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<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-error-msg">{{ errorMessage }}</div>
|
||||
<a-form
|
||||
ref="loginForm"
|
||||
:model="userInfo"
|
||||
class="login-form"
|
||||
layout="vertical"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<a-form-item
|
||||
field="username"
|
||||
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
>
|
||||
<a-input
|
||||
v-model="userInfo.username"
|
||||
:placeholder="$t('login.form.userName.placeholder')"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="password"
|
||||
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
>
|
||||
<a-input-password
|
||||
v-model="userInfo.password"
|
||||
:placeholder="$t('login.form.password.placeholder')"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-lock />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-space :size="16" direction="vertical">
|
||||
<div class="login-form-password-actions">
|
||||
<a-checkbox
|
||||
checked="rememberPassword"
|
||||
: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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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();
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
});
|
||||
const userInfo = reactive({
|
||||
username: loginConfig.value.username,
|
||||
password: loginConfig.value.password,
|
||||
});
|
||||
|
||||
const handleSubmit = async ({
|
||||
errors,
|
||||
values,
|
||||
}: {
|
||||
errors: Record<string, ValidatedError> | undefined;
|
||||
values: Record<string, any>;
|
||||
}) => {
|
||||
if (loading.value) return;
|
||||
if (!errors) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await userStore.login(values as LoginData);
|
||||
const { redirect, ...othersQuery } = router.currentRoute.value.query;
|
||||
router.push({
|
||||
name: (redirect as string) || 'DashboardOverview',
|
||||
query: {
|
||||
...othersQuery,
|
||||
},
|
||||
});
|
||||
Message.success(t('login.form.login.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 : '';
|
||||
} catch (err) {
|
||||
errorMessage.value = (err as Error).message;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
const setRememberPassword = (value: boolean) => {
|
||||
loginConfig.value.rememberPassword = value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.login-form {
|
||||
&-wrapper {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
&-sub-title {
|
||||
color: var(--color-text-3);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&-error-msg {
|
||||
height: 32px;
|
||||
color: rgb(var(--red-6));
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
&-password-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-register-btn {
|
||||
color: var(--color-text-3) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
frontend/platform_admin/src/views/login/index.vue
Normal file
81
frontend/platform_admin/src/views/login/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<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>
|
||||
</div>
|
||||
<LoginBanner />
|
||||
<div class="content">
|
||||
<div class="content-inner">
|
||||
<LoginForm />
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Footer from '@/components/footer/index.vue';
|
||||
import LoginBanner from './components/login-banner.vue';
|
||||
import LoginForm from './components/login-form.vue';
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
|
||||
.banner {
|
||||
width: 550px;
|
||||
background: linear-gradient(163.85deg, #1d2129 0%, #00308f 100%);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
left: 22px;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
color: var(--color-fill-1);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// responsive
|
||||
@media (max-width: @screen-lg) {
|
||||
.container {
|
||||
.banner {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
frontend/platform_admin/src/views/login/locale/en-US.ts
Normal file
21
frontend/platform_admin/src/views/login/locale/en-US.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
'login.form.title': 'Login to Arco Design Pro',
|
||||
'login.form.userName.errMsg': 'Username cannot be empty',
|
||||
'login.form.password.errMsg': 'Password cannot be empty',
|
||||
'login.form.login.errMsg': 'Login error, refresh and try again',
|
||||
'login.form.login.success': 'welcome to use',
|
||||
'login.form.userName.placeholder': 'Username: admin',
|
||||
'login.form.password.placeholder': 'Password: admin',
|
||||
'login.form.rememberPassword': 'Remember password',
|
||||
'login.form.forgetPassword': 'Forgot password',
|
||||
'login.form.login': 'login',
|
||||
'login.form.register': 'register account',
|
||||
'login.banner.slogan1': 'Out-of-the-box high-quality template',
|
||||
'login.banner.subSlogan1':
|
||||
'Rich page templates, covering most typical business scenarios',
|
||||
'login.banner.slogan2': 'Built-in solutions to common problems',
|
||||
'login.banner.subSlogan2':
|
||||
'Internationalization, routing configuration, state management everything',
|
||||
'login.banner.slogan3': 'Access visualization enhancement tool AUX',
|
||||
'login.banner.subSlogan3': 'Realize flexible block development',
|
||||
};
|
||||
19
frontend/platform_admin/src/views/login/locale/zh-CN.ts
Normal file
19
frontend/platform_admin/src/views/login/locale/zh-CN.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
'login.form.title': '登录 Arco Design Pro',
|
||||
'login.form.userName.errMsg': '用户名不能为空',
|
||||
'login.form.password.errMsg': '密码不能为空',
|
||||
'login.form.login.errMsg': '登录出错,轻刷新重试',
|
||||
'login.form.login.success': '欢迎使用',
|
||||
'login.form.userName.placeholder': '用户名:admin',
|
||||
'login.form.password.placeholder': '密码:admin',
|
||||
'login.form.rememberPassword': '记住密码',
|
||||
'login.form.forgetPassword': '忘记密码',
|
||||
'login.form.login': '登录',
|
||||
'login.form.register': '注册账号',
|
||||
'login.banner.slogan1': '开箱即用的高质量模板',
|
||||
'login.banner.subSlogan1': '丰富的的页面模板,覆盖大多数典型业务场景',
|
||||
'login.banner.slogan2': '内置了常见问题的解决方案',
|
||||
'login.banner.subSlogan2': '国际化,路由配置,状态管理应有尽有',
|
||||
'login.banner.slogan3': '接入可视化增强工具AUX',
|
||||
'login.banner.subSlogan3': '实现灵活的区块式开发',
|
||||
};
|
||||
30
frontend/platform_admin/src/views/not-found/index.vue
Normal file
30
frontend/platform_admin/src/views/not-found/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<a-result class="result" status="404" :subtitle="'not found'"> </a-result>
|
||||
<div class="operation-row">
|
||||
<a-button key="back" type="primary" @click="back"> back </a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const back = () => {
|
||||
// warning: Go to the node that has the permission
|
||||
router.push({ name: 'Workplace' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.content {
|
||||
// padding-top: 100px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -95px;
|
||||
margin-top: -121px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
<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>
|
||||
@@ -0,0 +1 @@
|
||||
<template><a-card title="菜单管理" :bordered="false"><a-tree :data="tree" :field-names="{ key: 'identity', title: 'name', children: 'children' }" /></a-card></template><script setup lang="ts">import { Message } from '@arco-design/web-vue'; import { computed, onMounted, ref } from 'vue'; import { platformApi, type PlatformMenu } from '@/api/platform'; type Node = PlatformMenu & { children: Node[] }; const list = ref<PlatformMenu[]>([]); const tree = computed<any[]>(() => { const byId = new Map<number, Node>(); const roots: Node[] = []; list.value.forEach((item) => byId.set(item.id, { ...item, children: [] })); byId.forEach((item) => { if (item.parent_id && byId.has(item.parent_id)) byId.get(item.parent_id)?.children.push(item); else roots.push(item); }); return roots; }); onMounted(async () => { try { list.value = (await platformApi.listMenu()).list; } catch (error) { Message.error((error as Error).message); } });</script>
|
||||
@@ -0,0 +1 @@
|
||||
<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>
|
||||
16
frontend/platform_admin/src/views/redirect/index.vue
Normal file
16
frontend/platform_admin/src/views/redirect/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const gotoPath = route.params.path as string;
|
||||
|
||||
router.replace({ path: gotoPath });
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
40
frontend/platform_admin/src/views/shared/CrudListPage.vue
Normal file
40
frontend/platform_admin/src/views/shared/CrudListPage.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<a-card :title="title" :bordered="false">
|
||||
<template #extra><a-space><a-button @click="load">刷新</a-button><a-button type="primary" @click="openCreate">新建</a-button></a-space></template>
|
||||
<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 fields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip />
|
||||
<a-table-column title="状态" data-index="status" :width="100"><template #cell="{ record }"><a-tag :color="record.status === 'enabled' ? 'green' : 'orange'">{{ record.status }}</a-tag></template></a-table-column>
|
||||
<a-table-column title="操作" :width="210" fixed="right"><template #cell="{ record }"><a-space><a-button size="mini" @click="openDetail(record)">详情</a-button><a-button size="mini" @click="openEdit(record)">编辑</a-button><a-button size="mini" @click="changeStatus(record)">{{ record.status === 'enabled' ? '停用' : '启用' }}</a-button></a-space></template></a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
<div class="pagination"><a-pagination :total="total" :current="page" :page-size="pageSize" show-total @change="changePage" /></div>
|
||||
</a-card>
|
||||
<a-drawer :visible="formVisible" :title="editingIdentity ? `编辑${title}` : `新建${title}`" :width="480" @cancel="formVisible = false" @ok="save">
|
||||
<a-form :model="form" layout="vertical"><a-form-item v-for="field in fields" :key="field.key" :label="field.label" :required="field.required"><a-input v-model="form[field.key]" :placeholder="`请输入${field.label}`" /></a-form-item></a-form>
|
||||
</a-drawer>
|
||||
<a-drawer :visible="detailVisible" title="详情" :width="540" @cancel="detailVisible = false"><a-descriptions :column="1" bordered><a-descriptions-item v-for="(value, key) in detail" :key="String(key)" :label="String(key)">{{ value ?? '-' }}</a-descriptions-item></a-descriptions></a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { resourceApi } from '@/api/resource';
|
||||
|
||||
type Field = { key: string; label: string; required?: boolean };
|
||||
type Row = Record<string, any>;
|
||||
const props = defineProps<{ title: string; resource: string; fields: Field[] }>();
|
||||
const loading = ref(false); const page = ref(1); const pageSize = 20; const total = ref(0); const list = ref<Row[]>([]);
|
||||
const formVisible = ref(false); const detailVisible = ref(false); const editingIdentity = ref(''); const form = reactive<Row>({}); const detail = ref<Row>({});
|
||||
function resetForm(data?: Row) { props.fields.forEach((field) => { form[field.key] = data?.[field.key] ?? ''; }); }
|
||||
async function load() { loading.value = true; try { const result = await resourceApi.list<Row>(props.resource, page.value, pageSize); list.value = result.list; total.value = result.total; } catch (error) { Message.error((error as Error).message); } finally { loading.value = false; } }
|
||||
function openCreate() { editingIdentity.value = ''; resetForm(); formVisible.value = true; }
|
||||
function openEdit(row: Row) { editingIdentity.value = row.identity; resetForm(row); formVisible.value = true; }
|
||||
function openDetail(row: Row) { detail.value = row; detailVisible.value = true; }
|
||||
async function save() { if (props.fields.some((field) => field.required && !form[field.key])) { Message.warning('请填写必填字段'); return; } try { if (editingIdentity.value) await resourceApi.update(props.resource, editingIdentity.value, form); else await resourceApi.create(props.resource, form); Message.success('保存成功'); formVisible.value = false; await load(); } catch (error) { Message.error((error as Error).message); } }
|
||||
async function changeStatus(row: Row) { try { await resourceApi.updateStatus(props.resource, row.identity, row.status === 'enabled' ? 'disabled' : 'enabled'); await load(); } catch (error) { Message.error((error as Error).message); } }
|
||||
async function changePage(next: number) { page.value = next; await load(); }
|
||||
onMounted(load);
|
||||
</script>
|
||||
<style scoped lang="less">.pagination { display: flex; justify-content: flex-end; margin-top: 16px; }</style>
|
||||
@@ -0,0 +1 @@
|
||||
<template><CrudListPage title="服务人员" resource="/staff/staff" :fields="fields" /></template><script setup lang="ts">import CrudListPage from '@/views/shared/CrudListPage.vue'; const fields = [{ key: 'name', label: '姓名', required: true }, { key: 'phone', label: '手机号' }, { key: 'role_code', label: '服务角色' }, { key: 'work_status', label: '工作状态' }];</script>
|
||||
1
frontend/platform_admin/src/views/user/list/ListPage.vue
Normal file
1
frontend/platform_admin/src/views/user/list/ListPage.vue
Normal file
@@ -0,0 +1 @@
|
||||
<template><CrudListPage title="业主客户" resource="/user/user" :fields="fields" /></template><script setup lang="ts">import CrudListPage from '@/views/shared/CrudListPage.vue'; const fields = [{ key: 'name', label: '姓名', required: true }, { key: 'phone', label: '手机号' }, { key: 'real_name', label: '实名名称' }];</script>
|
||||
Reference in New Issue
Block a user