yumi-admin/apps/src/views/operate/room-profile.vue
2026-04-13 19:31:36 +08:00

748 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import { approveData } from '#/api/legacy/approval';
import { OSS_FILE_BUCKETS, getAccessImgUrl, simpleUploadFile } from '#/api/legacy/oss';
import {
changeRole,
pofileTable,
removeOrPullBlackRoomUser,
updateProfile,
} from '#/api/legacy/room';
import AccountInput from '#/components/account-input.vue';
import { formatDate, getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
Form,
FormItem,
Image,
Input,
InputNumber,
Modal,
Select,
Space,
Table,
Tag,
TextArea,
message,
} from 'antdv-next';
import RoomDetailsDrawer from '#/views/app-system/components/room-details-drawer.vue';
import {
ROOM_EVENT_OPTIONS,
ROOM_ROLE_OPTIONS,
ROOM_USER_EVENT_OPTIONS,
} from './constants';
import { copyText } from './shared';
import RoomMemberDrawer from './components/room-member-drawer.vue';
import RoomOperationLogDrawer from './components/room-operation-log-drawer.vue';
import RoomSearchInput from './components/room-search-input.vue';
import RoomVisitorLogDrawer from './components/room-visitor-log-drawer.vue';
defineOptions({ name: 'OperateRoomProfile' });
function createEditForm() {
return {
event: 'AVAILABLE',
id: '',
maxAdmin: 0,
roomCover: '',
roomDesc: '',
roomName: '',
};
}
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const loading = ref(false);
const searchLoading = ref(false);
const list = ref<Array<Record<string, any>>>([]);
const listNotData = ref(false);
const loadMoreLoading = ref(false);
const editOpen = ref(false);
const uploadLoading = ref(false);
const editSaving = ref(false);
const editRow = ref<Record<string, any> | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const roomDetailsOpen = ref(false);
const roomMemberOpen = ref(false);
const visitorOpen = ref(false);
const operationLogOpen = ref(false);
const activeRoomId = ref<number | string>('');
const memberActionOpen = ref(false);
const memberActionSaving = ref(false);
const roleActionOpen = ref(false);
const roleActionSaving = ref(false);
const approvalOpen = ref(false);
const approvalLoading = ref(false);
const approvalState = ref<'NOT_PASS' | 'PASS'>('PASS');
const approvalType = ref('');
const approvalTitle = ref('');
const approvalContent = ref('');
const approvalRoomId = ref<number | string>('');
const approvalUserId = ref<number | string>('');
const query = reactive({
del: '0',
event: '',
lastId: '',
limit: 20,
roomAccount: '',
roomId: '',
sysOrigin: '',
userId: '',
});
const editForm = reactive(createEditForm());
const memberActionForm = reactive({
event: 'SYS_PULL_BLACK_ROOM_USER',
roomAccount: '',
sysOrigin: '',
userAccount: '',
});
const roleActionForm = reactive({
roles: 'MEMBER',
roomAccount: '',
sysOrigin: '',
userAccount: '',
});
const columns = [
{ dataIndex: 'room', key: 'room', title: '封面 / 房间信息', width: 320 },
{ dataIndex: 'base', key: 'base', title: '房间信息', width: 180 },
{ dataIndex: 'time', key: 'time', title: '时间', width: 220 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 260 },
];
const roomEventSelectOptions = ROOM_EVENT_OPTIONS.map((item) => ({
label: item.name,
value: item.value as any,
}));
const roomUserEventSelectOptions = ROOM_USER_EVENT_OPTIONS.map((item) => ({
label: item.name,
value: item.value as any,
}));
const roomRoleActionOptions = [
...ROOM_ROLE_OPTIONS.map((item) => ({
disabled: item.value === 'HOMEOWNER',
label: item.name,
value: item.value as any,
})),
{ label: '游客', value: 'TOURIST' as any },
];
const approvalStateOptions = [
{ label: '通过', value: 'PASS' as any },
{ label: '不通过', value: 'NOT_PASS' as any },
];
watch(
sysOriginOptions,
(options) => {
if (!query.sysOrigin && options.length > 0) {
query.sysOrigin = String(options[0]?.value || '');
void loadData(true);
}
},
{ immediate: true },
);
async function loadData(reset = false) {
if (!query.sysOrigin) {
return;
}
if (reset) {
list.value = [];
query.lastId = '';
listNotData.value = false;
}
loading.value = true;
try {
const result = (await pofileTable({ ...query })) || [];
list.value = reset ? result : [...list.value, ...result];
listNotData.value = result.length <= 0;
if (list.value.length > 0) {
query.lastId = String(list.value[list.value.length - 1]?.id || '');
}
} finally {
loading.value = false;
loadMoreLoading.value = false;
}
}
function handleSysOriginChange() {
query.roomId = '';
query.roomAccount = '';
searchLoading.value = false;
void loadData(true);
}
function loadSearchRoom() {
searchLoading.value = true;
}
function searchRoomSuccess(result: Record<string, any>) {
searchLoading.value = false;
query.roomId = String(result?.id || '');
query.roomAccount = '';
}
function searchRoomFail() {
query.roomId = '';
query.roomAccount = '';
searchLoading.value = false;
}
function openRoomDetails(roomId?: number | string) {
if (!roomId) {
return;
}
activeRoomId.value = roomId;
roomDetailsOpen.value = true;
}
function openRoomMembers(roomId?: number | string) {
if (!roomId) {
return;
}
activeRoomId.value = roomId;
roomMemberOpen.value = true;
}
function openVisitors(roomId?: number | string) {
if (!roomId) {
return;
}
activeRoomId.value = roomId;
visitorOpen.value = true;
}
function openOperationLogs(roomId?: number | string) {
if (!roomId) {
return;
}
activeRoomId.value = roomId;
operationLogOpen.value = true;
}
function openEdit(record: Record<string, any>) {
Object.assign(editForm, createEditForm(), {
event: record.event || 'AVAILABLE',
id: record.id || '',
maxAdmin: Number(record.setting?.maxAdmin || 0),
roomCover: record.roomCover || '',
roomDesc: record.roomDesc || '',
roomName: record.roomName || '',
});
editRow.value = record;
editOpen.value = true;
}
function openUpload() {
fileInputRef.value?.click();
}
async function handleUpload(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
target.value = '';
if (!file) {
return;
}
uploadLoading.value = true;
try {
const result = await simpleUploadFile(file, OSS_FILE_BUCKETS.other);
editForm.roomCover = getAccessImgUrl(result.name);
} finally {
uploadLoading.value = false;
}
}
async function handleSaveEdit() {
if (!editForm.id) {
return;
}
if (!String(editForm.roomName || '').trim()) {
message.warning('请输入房间名称');
return;
}
editSaving.value = true;
try {
await updateProfile({ ...editForm });
if (editRow.value) {
editRow.value.roomCover = editForm.roomCover;
editRow.value.roomDesc = editForm.roomDesc;
editRow.value.roomName = editForm.roomName;
editRow.value.event = editForm.event;
editRow.value.setting = {
...(editRow.value.setting || {}),
maxAdmin: editForm.maxAdmin,
};
}
message.success('保存成功');
editOpen.value = false;
} finally {
editSaving.value = false;
}
}
function openApproval(record: Record<string, any>, type: string) {
approvalType.value = type;
approvalRoomId.value = record.id;
approvalUserId.value = record.userId;
approvalState.value = 'PASS';
if (type === 'ROOM_NICKNAME') {
approvalTitle.value = '房间昵称审批';
approvalContent.value = record.roomName || '';
} else if (type === 'ROOM_AVATAR') {
approvalTitle.value = '房间头像审批';
approvalContent.value = record.roomCover || '';
} else {
approvalTitle.value = '房间公告审批';
approvalContent.value = record.roomDesc || '';
}
approvalOpen.value = true;
}
async function handleApproval() {
if (!approvalType.value || !approvalRoomId.value || !approvalUserId.value) {
return;
}
approvalLoading.value = true;
try {
await approveData({
approvalStatus: approvalState.value,
approvalType: approvalType.value,
waitApprovalUser: [
{
content: approvalContent.value,
contentId: approvalRoomId.value,
userId: approvalUserId.value,
},
],
});
message.success('审批成功');
approvalOpen.value = false;
await loadData(true);
} finally {
approvalLoading.value = false;
}
}
function openMemberAction() {
memberActionForm.event = 'SYS_PULL_BLACK_ROOM_USER';
memberActionForm.roomAccount = '';
memberActionForm.sysOrigin = query.sysOrigin;
memberActionForm.userAccount = '';
memberActionOpen.value = true;
}
async function handleMemberAction() {
if (!memberActionForm.roomAccount || !memberActionForm.userAccount) {
message.warning('请输入房间账号和用户账号');
return;
}
memberActionSaving.value = true;
try {
await removeOrPullBlackRoomUser({ ...memberActionForm, sysOrigin: query.sysOrigin });
message.success('操作成功');
memberActionOpen.value = false;
await loadData(true);
} finally {
memberActionSaving.value = false;
}
}
function openRoleAction() {
roleActionForm.roles = 'MEMBER';
roleActionForm.roomAccount = '';
roleActionForm.sysOrigin = query.sysOrigin;
roleActionForm.userAccount = '';
roleActionOpen.value = true;
}
async function handleRoleAction() {
if (!roleActionForm.roomAccount || !roleActionForm.userAccount) {
message.warning('请输入房间账号和用户账号');
return;
}
roleActionSaving.value = true;
try {
await changeRole({ ...roleActionForm, sysOrigin: query.sysOrigin });
message.success('操作成功');
roleActionOpen.value = false;
await loadData(true);
} finally {
roleActionSaving.value = false;
}
}
async function handleCopy(value: number | string) {
if (!value) {
return;
}
try {
await copyText(String(value));
message.success('复制成功');
} catch {
message.error('复制失败');
}
}
</script>
<template>
<Page title="房间资料管理">
<Card>
<div class="toolbar">
<Space wrap>
<SysOriginSelect
v-model:value="query.sysOrigin"
style="width: 140px"
@change="handleSysOriginChange"
:options="sysOriginOptions"
></SysOriginSelect>
<Select option-label-prop="label"
v-model:value="query.del"
style="width: 120px"
@change="loadData(true)"
:options="[{ label: '未注销', value: '0' as any }, { label: '已注销', value: '1' as any }]"
/>
<Select option-label-prop="label"
v-model:value="query.event"
allow-clear
placeholder="房间状态"
style="width: 140px"
:options="ROOM_EVENT_OPTIONS.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
/>
<div class="room-search-field">
<RoomSearchInput
:key="query.sysOrigin"
:sys-origin="query.sysOrigin"
placeholder="房间ID"
@fail="searchRoomFail"
@load="loadSearchRoom"
@success="searchRoomSuccess"
/>
</div>
<AccountInput
v-model:value="query.userId"
:sys-origin="query.sysOrigin"
placeholder="用户ID"
style="width: 280px"
/>
<Button :loading="searchLoading || loading" type="primary" @click="loadData(true)">
搜索
</Button>
<Button @click="openMemberAction">移除/拉黑成员</Button>
<Button @click="openRoleAction">权限变更</Button>
</Space>
</div>
<Table
:columns="columns"
:data-source="list"
:loading="loading"
:pagination="false"
row-key="id"
:scroll="{ x: 980 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'room'">
<div class="room-cell">
<Image
:src="record.roomCover || 'https://dummyimage.com/72x72/e2e8f0/64748b&text=R'"
class="room-cover"
/>
<div class="room-copy">
<Button size="small" type="link" @click="openRoomDetails(record.id)">
{{ record.roomName || '-' }}
</Button>
<div>账号{{ record.roomAccount || '-' }}</div>
<div class="room-desc">{{ record.roomDesc || '-' }}</div>
</div>
</div>
</template>
<template v-else-if="column.key === 'base'">
<div>状态
<Tag>{{ ROOM_EVENT_OPTIONS.find((item) => item.value === record.event)?.name || record.event || '-' }}</Tag>
</div>
<div>ID{{ record.id || '-' }}</div>
<div>管理员上限{{ record.setting?.maxAdmin ?? '-' }}</div>
</template>
<template v-else-if="column.key === 'time'">
<div>创建{{ formatDate(record.createTime) }}</div>
<div>修改{{ formatDate(record.updateTime) }}</div>
</template>
<template v-else-if="column.key === 'actions'">
<Space wrap>
<Button size="small" type="link" @click="handleCopy(record.id)">复制ID</Button>
<Button size="small" type="link" @click="openEdit(record)">编辑资料</Button>
<Button size="small" type="link" @click="openRoomMembers(record.id)">成员列表</Button>
<Button size="small" type="link" @click="openVisitors(record.id)">访客记录</Button>
<Button size="small" type="link" @click="openOperationLogs(record.id)">操作日志</Button>
<Button size="small" type="link" @click="openApproval(record, 'ROOM_NICKNAME')">
审批昵称
</Button>
<Button size="small" type="link" @click="openApproval(record, 'ROOM_AVATAR')">
审批头像
</Button>
<Button size="small" type="link" @click="openApproval(record, 'ROOM_NOTICE')">
审批公告
</Button>
</Space>
</template>
</template>
</Table>
<div class="load-more">
<span v-if="listNotData">已加载全部</span>
<Button
v-else
:loading="loadMoreLoading"
size="small"
@click="loadMoreLoading = true; loadData()"
>
点击加载更多
</Button>
</div>
</Card>
<RoomDetailsDrawer
:open="roomDetailsOpen"
:room-id="activeRoomId"
@close="roomDetailsOpen = false"
/>
<RoomMemberDrawer
:open="roomMemberOpen"
:room-id="activeRoomId"
:sys-origin="query.sysOrigin"
@close="roomMemberOpen = false"
/>
<RoomVisitorLogDrawer
:open="visitorOpen"
:room-id="activeRoomId"
:sys-origin="query.sysOrigin"
@close="visitorOpen = false"
/>
<RoomOperationLogDrawer
:open="operationLogOpen"
:room-id="activeRoomId"
@close="operationLogOpen = false"
/>
<Modal
:confirm-loading="editSaving"
:open="editOpen"
destroy-on-close
title="编辑房间资料"
width="520"
@cancel="editOpen = false"
@ok="handleSaveEdit"
>
<Form layout="vertical">
<FormItem label="封面">
<div class="upload-row">
<Image
:src="editForm.roomCover || 'https://dummyimage.com/88x88/e2e8f0/64748b&text=R'"
class="upload-preview"
/>
<div>
<Button :loading="uploadLoading" @click="openUpload">上传封面</Button>
<input ref="fileInputRef" accept="image/*" hidden type="file" @change="handleUpload">
</div>
</div>
</FormItem>
<FormItem label="名称">
<Input v-model:value="editForm.roomName" :maxlength="24" />
</FormItem>
<FormItem label="公告">
<TextArea
v-model:value="editForm.roomDesc"
:maxlength="160"
:rows="4"
/>
</FormItem>
<FormItem label="状态">
<Select
option-label-prop="label"
v-model:value="editForm.event"
:options="roomEventSelectOptions"
/>
</FormItem>
<FormItem label="管理员数量">
<InputNumber
v-model:value="editForm.maxAdmin"
:min="0"
:precision="0"
style="width: 100%"
/>
</FormItem>
</Form>
</Modal>
<Modal
:confirm-loading="memberActionSaving"
:open="memberActionOpen"
destroy-on-close
title="移除 / 拉黑成员"
@cancel="memberActionOpen = false"
@ok="handleMemberAction"
>
<Form layout="vertical">
<FormItem label="平台">
<Input :value="query.sysOrigin" disabled />
</FormItem>
<FormItem label="房间账号">
<Input v-model:value="memberActionForm.roomAccount" />
</FormItem>
<FormItem label="用户账号">
<Input v-model:value="memberActionForm.userAccount" />
</FormItem>
<FormItem label="事件">
<Select
option-label-prop="label"
v-model:value="memberActionForm.event"
:options="roomUserEventSelectOptions"
/>
</FormItem>
</Form>
</Modal>
<Modal
:confirm-loading="roleActionSaving"
:open="roleActionOpen"
destroy-on-close
title="房间内权限变更"
@cancel="roleActionOpen = false"
@ok="handleRoleAction"
>
<Form layout="vertical">
<FormItem label="平台">
<Input :value="query.sysOrigin" disabled />
</FormItem>
<FormItem label="房间账号">
<Input v-model:value="roleActionForm.roomAccount" />
</FormItem>
<FormItem label="用户账号">
<Input v-model:value="roleActionForm.userAccount" />
</FormItem>
<FormItem label="权限">
<Select
option-label-prop="label"
v-model:value="roleActionForm.roles"
:options="roomRoleActionOptions"
/>
</FormItem>
</Form>
</Modal>
<Modal
:confirm-loading="approvalLoading"
:open="approvalOpen"
destroy-on-close
:title="approvalTitle"
ok-text="提交"
@cancel="approvalOpen = false"
@ok="handleApproval"
>
<Form layout="vertical">
<FormItem label="审批状态">
<Select
option-label-prop="label"
v-model:value="approvalState"
:options="approvalStateOptions"
/>
</FormItem>
<FormItem label="审批内容">
<div v-if="approvalType === 'ROOM_AVATAR'" class="approval-preview">
<Image :src="approvalContent" class="approval-image" />
</div>
<TextArea v-else :rows="4" :value="approvalContent" disabled />
</FormItem>
</Form>
</Modal>
</Page>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.room-search-field {
width: 360px;
}
.room-cell {
align-items: flex-start;
display: flex;
gap: 14px;
}
.room-cover {
border-radius: 16px;
flex-shrink: 0;
height: 72px;
object-fit: cover;
width: 72px;
}
.room-copy {
min-width: 0;
}
.room-desc {
color: #64748b;
margin-top: 4px;
word-break: break-word;
}
.load-more {
display: flex;
justify-content: center;
margin-top: 16px;
}
.upload-row {
align-items: center;
display: flex;
gap: 16px;
}
.upload-preview {
border-radius: 16px;
height: 88px;
object-fit: cover;
width: 88px;
}
.approval-preview {
display: flex;
justify-content: center;
}
.approval-image {
border-radius: 16px;
max-height: 240px;
max-width: 240px;
object-fit: cover;
}
</style>