This commit is contained in:
孟帅
2024-04-22 23:08:40 +08:00
parent 82483bd7b9
commit e144b12580
445 changed files with 17457 additions and 6708 deletions

View File

@@ -0,0 +1,76 @@
<template>
<n-alert :show-icon="false" title="说明">
<n-p
>这里主要演示多租户业务下不同用户身份如何在同一页面下展示不同的表格功能和字段数据以及添加/编辑购买订单时服务端如何自动维护多租户关系</n-p
>
<n-p style="font-weight: 600">不同身份的测试账号</n-p>
<n-table :bordered="false" :single-line="false" size="small">
<thead>
<tr>
<th class="table-center">身份</th>
<th class="table-center">ID</th>
<th class="table-center">账号</th>
<th class="table-center">密码</th>
<th>身份描述</th>
</tr>
</thead>
<tbody>
<tr v-for="account in accounts" :key="account.id">
<td class="table-center">{{ account.type }}</td>
<td class="table-center">{{ account.id }}</td>
<td class="table-center">{{ account.username }}</td>
<td class="table-center">{{ account.password }}</td>
<td>{{ account.dc }}</td>
</tr>
</tbody>
</n-table>
</n-alert>
</template>
<script setup lang="ts">
interface Account {
type: string;
id: number;
username: string;
password: string;
dc: string;
}
const accounts: Account[] = [
{
type: '公司',
id: 1,
username: 'admin',
password: '123456',
dc: '可见全部数据。管理整个平台,包括商户和用户账户',
},
{
type: '租户',
id: 8,
username: 'ameng',
password: '123456',
dc: '可见自己下面的商户和用户数据。多租户系统中顶层实体,有自己的多个商户、用户、产品、订单等',
},
{
type: '商户',
id: 11,
username: 'abai',
password: '123456',
dc: '可见自己下面的用户数据。受租户的监管和管理,可以独立经营的实体,提供产品或服务,管理自己的业务,包括库存管理、订单处理、结算等',
},
{
type: '用户',
id: 12,
username: 'asong',
password: '123456',
dc: '只能看到自己数据。真正购买产品或享受服务的人,与商户互动,管理个人信息等个性化功能',
},
];
</script>
<style scoped lang="less">
.table-center {
text-align: center;
min-width: 80px;
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
:title="formValue.id > 0 ? '编辑购买订单 #' + formValue.id : '添加购买订单'"
:style="{
width: dialogWidth,
}"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-spin :show="loading" description="请稍候...">
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
:label-placement="settingStore.isMobile ? 'top' : 'left'"
:label-width="100"
class="py-4"
>
<n-grid cols="1 s:1 m:1 l:1 xl:1 2xl:1" responsive="screen">
<n-gi span="1" v-if="userStore.isCompanyDept">
<n-form-item label="租户ID" path="tenantId">
<n-input placeholder="请输入租户ID" v-model:value="formValue.tenantId" />
</n-form-item>
</n-gi>
<n-gi span="1" v-if="userStore.isCompanyDept || userStore.isTenantDept">
<n-form-item label="商户ID" path="merchantId">
<n-input placeholder="请输入商户ID" v-model:value="formValue.merchantId" />
</n-form-item>
</n-gi>
<n-gi
span="1"
v-if="userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept"
>
<n-form-item label="用户ID" path="userId">
<n-input placeholder="请输入用户ID" v-model:value="formValue.userId" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="购买产品" path="productName">
<n-input placeholder="请输入购买产品" v-model:value="formValue.productName" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="关联订单号" path="orderSn">
<n-input placeholder="请输入关联订单号" v-model:value="formValue.orderSn" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="充值金额" path="money">
<n-input-group>
<n-input-number
:min="1"
:show-button="false"
style="width: 100%"
placeholder="请输入充值金额"
v-model:value="formValue.money"
/>
<n-input-group-label></n-input-group-label>
</n-input-group>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="备注" path="remark">
<n-input placeholder="请输入备注" v-model:value="formValue.remark" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="支付状态" path="status">
<n-select v-model:value="formValue.status" :options="options.payStatus" />
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-spin>
</n-scrollbar>
<template #action>
<n-space>
<n-button @click="closeForm"> 取消 </n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm"> 确定 </n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { Edit, View } from '@/api/addons/hgexample/tenantOrder';
import { options, State, newState, rules } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { useUserStore } from '@/store/modules/user';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const userStore = useUserStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth(840);
});
function openModal(state: State) {
showModal.value = true;
// 新增
if (!state || state.id < 1) {
formValue.value = newState(state);
return;
}
// 编辑
loading.value = true;
View({ id: state.id })
.then((res) => {
formValue.value = res;
})
.finally(() => {
loading.value = false;
});
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formValue.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
closeForm();
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
showModal.value = false;
loading.value = false;
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,146 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="多租户功能演示">
<Alert />
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
ref="actionRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
:actionColumn="actionColumn"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="primary"
@click="addTable"
class="min-left-space"
v-if="hasPermission(['/hgexample/tenantOrder/edit'])"
>
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加购买订单
</n-button>
</template>
</BasicTable>
</n-card>
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref, computed, onMounted } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Delete } from '@/api/addons/hgexample/tenantOrder';
import { PlusOutlined } from '@vicons/antd';
import { columns, schemas, loadOptions } from './model';
import { adaTableScrollX } from '@/utils/hotgo';
import Edit from './edit.vue';
import Alert from './alert.vue';
const dialog = useDialog();
const message = useMessage();
const { hasPermission } = usePermission();
const actionRef = ref();
const searchFormRef = ref<any>({});
const editRef = ref();
const actionColumn = reactive({
width: 144,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/hgexample/tenantOrder/edit'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/hgexample/tenantOrder/delete'],
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载表格数据
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
// 重新加载表格数据
function reloadTable() {
actionRef.value?.reload();
}
// 添加数据
function addTable() {
editRef.value.openModal(null);
}
// 编辑数据
function handleEdit(record: Recordable) {
editRef.value.openModal(record);
}
// 单个删除
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
});
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,243 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { Option, getOptionLabel, getOptionTag } from '@/utils/hotgo';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
export class State {
public id = 0; // 主键
public tenantId = null; // 租户ID
public merchantId = null; // 商户ID
public userId = null; // 用户ID
public productName = ''; // 购买产品
public orderSn = ''; // 关联订单号
public money = null; // 充值金额
public remark = ''; // 备注
public status = 1; // 订单状态
public createdAt = ''; // 创建时间
public updatedAt = ''; // 修改时间
constructor(state?: Partial<State>) {
if (state) {
Object.assign(this, state);
}
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) {
return cloneDeep(state);
}
return new State(state);
}
return new State();
}
// 表单验证规则
export const rules = {
money: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入充值金额',
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'tenantId',
component: 'NInput',
label: '租户ID',
componentProps: {
placeholder: '请输入租户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept;
},
},
{
field: 'merchantId',
component: 'NInput',
label: '商户ID',
componentProps: {
placeholder: '请输入商户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept;
},
},
{
field: 'userId',
component: 'NInput',
label: '用户ID',
componentProps: {
placeholder: '请输入用户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept;
},
},
{
field: 'orderSn',
component: 'NInput',
label: '订单号',
componentProps: {
placeholder: '请输入订单号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '订单状态',
defaultValue: null,
componentProps: {
placeholder: '请选择订单状态',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
// 表格列
export const columns = [
{
title: '订单ID',
key: 'id',
align: 'left',
width: 100,
},
{
title: '租户ID',
key: 'tenantId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept;
},
},
{
title: '商户ID',
key: 'merchantId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept;
},
},
{
title: '用户ID',
key: 'userId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept;
},
},
{
title: '购买产品',
key: 'productName',
align: 'left',
width: 150,
},
{
title: '订单号',
key: 'orderSn',
align: 'left',
width: 200,
},
{
title: '充值金额',
key: 'money',
align: 'left',
width: 100,
render(row) {
return row.money + ' 元';
},
},
{
title: '订单状态',
key: 'status',
align: 'left',
width: 100,
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.payStatus, row.status),
bordered: false,
},
{
default: () => getOptionLabel(options.value.payStatus, row.status),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
align: 'left',
width: 180,
},
];
// 字典数据选项
export const options = ref({
payStatus: [] as Option[],
});
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['payStatus'],
}).then((res) => {
options.value = res;
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.payStatus;
break;
}
}
});
}