feat: add invite user binding

This commit is contained in:
richjohnrambo
2026-09-11 10:46:20 +08:00
parent 68d9bafbdd
commit 2fb5102add
18 changed files with 452 additions and 73 deletions
+8
View File
@@ -40,6 +40,14 @@ export function Revoke(params) {
});
}
export function BindUser(params) {
return http.request({
url: '/imInvite/bindUser',
method: 'POST',
params,
});
}
export function BatchRevoke(params) {
return http.request({
url: '/imInvite/batchRevoke',
@@ -1,7 +1,6 @@
<template>
<n-modal v-model:show="showModal" preset="card" title="批次详情" class="batch-view-modal">
<n-descriptions :column="4" bordered size="small">
<n-descriptions-item label="批次ID">{{ detail.batchId || '-' }}</n-descriptions-item>
<n-descriptions-item label="总数">{{ detail.quantity || 0 }}</n-descriptions-item>
<n-descriptions-item label="有效">{{ detail.activeCount || 0 }}</n-descriptions-item>
<n-descriptions-item label="累计注册次数">{{ detail.usageCount || 0 }}</n-descriptions-item>
@@ -27,6 +26,16 @@
{ title: '邀请码', key: 'code' },
{ title: '状态', key: 'status' },
{ title: '累计使用次数', key: 'usageCount' },
{
title: '可用次数',
key: 'availableUses',
render(row: any) {
if (!row.maxUses || Number(row.maxUses) <= 0) {
return '不限制';
}
return `${Math.max(Number(row.maxUses) - Number(row.usageCount || 0), 0)}`;
},
},
{ title: '最后使用时间', key: 'lastUsedAt' },
];
@@ -0,0 +1,167 @@
<template>
<n-modal v-model:show="showModal" preset="card" title="绑定用户" class="bind-user-modal">
<n-form :model="searchForm" label-placement="left" label-width="90">
<n-grid :cols="2" :x-gap="12">
<n-form-item-gi label="用户ID">
<n-input v-model:value="searchForm.userId" clearable placeholder="用户ID/编号" />
</n-form-item-gi>
<n-form-item-gi label="关键词">
<n-input v-model:value="searchForm.keyword" clearable placeholder="用户名/昵称/手机号" />
</n-form-item-gi>
</n-grid>
<n-space>
<n-button type="primary" :loading="loading" @click="handleSearch">查询</n-button>
<n-button @click="handleReset">重置</n-button>
</n-space>
</n-form>
<n-data-table
class="user-table"
:columns="columns"
:data="users"
:loading="loading"
:row-key="(row) => row.userId"
:checked-row-keys="checkedRowKeys"
:pagination="pagination"
@update:checked-row-keys="handleChecked"
@update:page="handlePageChange"
/>
<template #footer>
<n-space justify="end">
<n-button @click="showModal = false">取消</n-button>
<n-button type="primary" :disabled="!selectedUserId" :loading="submitting" @click="handleSubmit">
确认绑定
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script lang="ts" setup>
import { computed, h, reactive, ref } from 'vue';
import { NAvatar, useMessage } from 'naive-ui';
import { List as UserList } from '@/api/imUser';
import { BindUser } from '@/api/im/registrationInvite';
import defaultAvatar from '@/assets/images/schoolboy.png';
const emit = defineEmits(['reload']);
const message = useMessage();
const showModal = ref(false);
const loading = ref(false);
const submitting = ref(false);
const tenantId = ref(0);
const inviteId = ref('');
const users = ref<any[]>([]);
const checkedRowKeys = ref<string[]>([]);
const searchForm = reactive({ userId: '', keyword: '' });
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0, showSizePicker: false });
const selectedUserId = computed(() => checkedRowKeys.value[0] || '');
const columns = [
{ type: 'selection', multiple: false },
{ title: '编号', key: 'displayNo', width: 90 },
{
title: '头像',
key: 'avatar',
width: 70,
render(row: any) {
return h(NAvatar, { size: 36, src: row.avatar || defaultAvatar });
},
},
{ title: '用户ID', key: 'userId', width: 130 },
{ title: '用户名', key: 'username', width: 140 },
{
title: '昵称',
key: 'nickname',
width: 140,
render(row: any) {
return row.nickname || '--';
},
},
{
title: '手机号',
key: 'phone',
width: 150,
render(row: any) {
return row.phone || '--';
},
},
];
async function loadUsers() {
loading.value = true;
try {
const res = await UserList({
tenantId: tenantId.value,
userId: searchForm.userId,
keyword: searchForm.keyword,
page: pagination.page,
perPage: pagination.pageSize,
});
users.value = res?.list || [];
pagination.itemCount = Number(res?.total || 0);
} finally {
loading.value = false;
}
}
function handleChecked(keys: string[]) {
checkedRowKeys.value = keys.slice(-1);
}
function handlePageChange(page: number) {
pagination.page = page;
loadUsers();
}
function handleSearch() {
pagination.page = 1;
checkedRowKeys.value = [];
loadUsers();
}
function handleReset() {
searchForm.userId = '';
searchForm.keyword = '';
handleSearch();
}
async function handleSubmit() {
if (!selectedUserId.value) {
message.warning('请选择要绑定的用户');
return;
}
submitting.value = true;
try {
await BindUser({ tenantId: tenantId.value, id: inviteId.value, userId: Number(selectedUserId.value) });
message.success('绑定成功');
showModal.value = false;
emit('reload');
} finally {
submitting.value = false;
}
}
function openModal(rowTenantId: number, rowInviteId: string) {
tenantId.value = rowTenantId;
inviteId.value = rowInviteId;
searchForm.userId = '';
searchForm.keyword = '';
users.value = [];
checkedRowKeys.value = [];
pagination.page = 1;
pagination.itemCount = 0;
showModal.value = true;
}
defineExpose({ openModal });
</script>
<style lang="less" scoped>
.bind-user-modal {
width: min(980px, 94vw);
}
.user-table {
margin-top: 16px;
}
</style>
+15 -2
View File
@@ -45,6 +45,7 @@
</n-card>
<CreateModal ref="createRef" @reload="reloadTable" />
<BatchViewModal ref="batchViewRef" />
<BindUserModal ref="bindUserRef" @reload="reloadTable" />
</div>
</template>
@@ -69,6 +70,7 @@
import { adaTableScrollX } from '@/utils/hotgo';
import CreateModal from './CreateModal.vue';
import BatchViewModal from './BatchViewModal.vue';
import BindUserModal from './BindUserModal.vue';
const message = useMessage();
const dialog = useDialog();
@@ -78,6 +80,7 @@
const searchFormRef = ref<any>({});
const createRef = ref();
const batchViewRef = ref();
const bindUserRef = ref();
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
const tableColumns = computed(() =>
@@ -98,7 +101,7 @@
);
const actionColumn = reactive({
width: 300,
width: 380,
title: '操作',
key: 'action',
fixed: 'right',
@@ -106,6 +109,12 @@
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '绑定用户',
onClick: handleBindUser.bind(null, record),
auth: ['/imInvite/bindUser'],
ifShow: () => record.status === 'active' && !record.boundUserId && Number(record.usageCount || 0) === 0,
},
{
label: '批次',
onClick: handleBatchView.bind(null, record),
@@ -168,6 +177,10 @@
batchViewRef.value.openModal(Number(record.tenantId || selectedTenantId()), record.batchId);
}
function handleBindUser(record: State) {
bindUserRef.value.openModal(Number(record.tenantId || selectedTenantId()), record.id);
}
function handleRevoke(record: State) {
dialog.warning({
title: '确认撤销',
@@ -185,7 +198,7 @@
function handleBatchRevoke(record: State) {
dialog.warning({
title: '确认撤销批次',
content: `将撤销批次 ${record.batchId} 下仍有效的邀请码,已过期或已撤销的邀请码不受影响,是否确认?`,
content: '将撤销该批次下仍有效的邀请码,已过期或已撤销的邀请码不受影响,是否确认?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
+28 -9
View File
@@ -16,7 +16,10 @@ export class State {
public code = '';
public status = '';
public expiresAt = '';
public maxUses = 0;
public usageCount = 0;
public boundUserId = '';
public boundUserLabel = '';
public lastUsedAt = '';
public createdBy = '';
public remark = '';
@@ -59,14 +62,6 @@ export const schemas = ref<FormSchema[]>([
placeholder: '请选择状态',
},
},
{
field: 'batchId',
component: 'NInput',
label: '批次ID',
componentProps: {
placeholder: '请输入批次ID',
},
},
{
field: 'createdBy',
component: 'NInput',
@@ -102,9 +97,26 @@ export const columns = [
{ title: '商户', key: 'tenantName', align: 'left', width: 160 },
{ title: '邀请码', key: 'code', align: 'left', width: 120 },
{ title: '状态', key: 'status', align: 'left', width: 110 },
{ title: '批次ID', key: 'batchId', align: 'left', width: 150 },
{ title: '到期时间', key: 'expiresAt', align: 'left', width: 190 },
{ title: '累计使用次数', key: 'usageCount', align: 'left', width: 130 },
{
title: '可用次数',
key: 'availableUses',
align: 'left',
width: 110,
render(row: State) {
return inviteAvailableUsesLabel(row);
},
},
{
title: '绑定用户',
key: 'boundUserLabel',
align: 'left',
width: 180,
render(row: State) {
return row.boundUserLabel || row.boundUserId || '--';
},
},
{ title: '最后使用时间', key: 'lastUsedAt', align: 'left', width: 190 },
{ title: '创建人', key: 'createdBy', align: 'left', width: 110 },
{ title: '创建时间', key: 'createdAt', align: 'left', width: 190 },
@@ -128,6 +140,13 @@ export function statusLabel(status: string) {
return statusOptions.find((item) => item.value === status)?.label || status;
}
export function inviteAvailableUsesLabel(row: State) {
if (!row.maxUses || Number(row.maxUses) <= 0) {
return '不限制';
}
return `${Math.max(Number(row.maxUses) - Number(row.usageCount || 0), 0)}`;
}
export function localRfc3339(value: number | null) {
if (!value) {
return '';