yumi-admin/apps/src/views/dynamic/user-dynamic-list.vue
2026-03-31 17:40:10 +08:00

519 lines
12 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 { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
closeUserDynamicTop,
deleteUserDynamic,
setUserDynamicTop,
userDynamicTable,
} from '#/api/legacy/dynamic';
import AccountInput from '#/components/account-input.vue';
import { formatDate,
getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
Checkbox,
Col,
DateRangePicker,
Empty,
Image,
InputNumber,
Modal,
Pagination,
Row,
Select,
Space,
Spin,
Tag,
message
} from 'antdv-next';
import AccountHandleModal from '#/views/approval/components/account-handle-modal.vue';
defineOptions({ name: 'DynamicUserDynamicList' });
const router = useRouter();
const accessStore = useAccessStore();
const sysOriginOptions = getAllowedSysOrigins(accessStore.accessCodes || []);
const loading = ref(false);
const total = ref(0);
const list = ref<Array<Record<string, any>>>([]);
const rangeDate = ref<[string, string] | null>(null);
const selectedKeys = ref<string[]>([]);
const topModalOpen = ref(false);
const topSaving = ref(false);
const accountHandleOpen = ref(false);
const activeUserId = ref('');
const topForm = reactive({
dynamicId: '',
sysOrigin: '',
weights: undefined as number | undefined,
});
const query = reactive({
cursor: 1,
endTime: '',
limit: 30,
startTime: '',
sysOrigin: sysOriginOptions[0]?.value ?? 'LIKEI',
top: '',
userId: '',
});
watch(rangeDate, (value) => {
query.startTime = value?.[0] || '';
query.endTime = value?.[1] || '';
});
const allChecked = computed(
() => list.value.length > 0 && selectedKeys.value.length === list.value.length,
);
const indeterminate = computed(
() =>
selectedKeys.value.length > 0 &&
selectedKeys.value.length < list.value.length,
);
function getPictures(record: Record<string, any>) {
return (Array.isArray(record.pictures) ? record.pictures : [])
.map((item: any) => item?.resourceUrl || item?.url || '')
.filter(Boolean);
}
function getUserAvatar(record: Record<string, any>) {
return record.userBaseInfo?.userAvatar || record.userAvatar || '';
}
function getUserName(record: Record<string, any>) {
return record.userBaseInfo?.userNickname || record.userNickname || '-';
}
function getUserAccount(record: Record<string, any>) {
return record.userBaseInfo?.actualAccount || record.actualAccount || '-';
}
function getRowKey(record: Record<string, any>) {
return String(record.id || '');
}
async function loadData(reset = false) {
if (reset) {
query.cursor = 1;
}
loading.value = true;
try {
const result = await userDynamicTable({ ...query });
list.value = result.records || [];
total.value = result.total || 0;
selectedKeys.value = [];
} finally {
loading.value = false;
}
}
function handleSearch() {
loadData(true);
}
function handlePageChange(page: number, pageSize: number) {
query.cursor = page;
query.limit = pageSize;
loadData();
}
function toggleAll(checked: boolean) {
selectedKeys.value = checked ? list.value.map((item) => getRowKey(item)) : [];
}
function toggleRecord(record: Record<string, any>) {
const key = getRowKey(record);
const index = selectedKeys.value.findIndex((item) => item === key);
if (index >= 0) {
selectedKeys.value.splice(index, 1);
return;
}
selectedKeys.value.push(key);
}
function openUserDetails(record: Record<string, any>) {
const userId = record.userId || record.userBaseInfo?.id;
if (!userId) {
return;
}
router.push(`/common/user/deatils/${userId}`);
}
function openAccountHandle(record: Record<string, any>) {
activeUserId.value = String(record.userId || record.userBaseInfo?.id || '');
accountHandleOpen.value = true;
}
function openTopEdit(record: Record<string, any>) {
topForm.dynamicId = String(record.id || '');
topForm.sysOrigin = query.sysOrigin;
topForm.weights = Number(record.weights || 0);
topModalOpen.value = true;
}
async function handleCloseTop(record: Record<string, any>) {
await closeUserDynamicTop(record.id);
message.success('操作成功');
await loadData(true);
}
async function handleSubmitTop() {
if (!topForm.dynamicId) {
return;
}
if (topForm.weights === undefined || topForm.weights === null) {
message.warning('请输入权重');
return;
}
topSaving.value = true;
try {
await setUserDynamicTop({
dynamicId: topForm.dynamicId,
sysOrigin: topForm.sysOrigin,
weights: topForm.weights,
});
message.success('保存成功');
topModalOpen.value = false;
await loadData(true);
} finally {
topSaving.value = false;
}
}
function handleDeleteSelected() {
if (selectedKeys.value.length === 0) {
message.warning('请勾选审批项');
return;
}
Modal.confirm({
title: '确认删除选中记录吗?',
async onOk() {
await deleteUserDynamic(selectedKeys.value);
message.success('删除成功');
await loadData();
},
});
}
loadData(true);
</script>
<template>
<Page title="用户动态">
<div class="page-grid">
<Card>
<div class="toolbar">
<Space wrap>
<SysOriginSelect
v-model:value="query.sysOrigin"
style="width: 140px"
@change="handleSearch"
:options="sysOriginOptions"
></SysOriginSelect>
<Select
v-model:value="query.top"
option-label-prop="label"
style="width: 140px"
@change="handleSearch"
:options="[{ label: '全部', value: '' as any }, { label: '已置顶', value: '1' as any }, { label: '未置顶', value: '0' as any }]"
/>
<DateRangePicker
v-model:value="rangeDate"
show-time
style="width: 360px"
value-format="x"
/>
<AccountInput
v-model:value="query.userId"
:sys-origin="query.sysOrigin"
placeholder="用户ID"
style="width: 220px"
/>
<Button :loading="loading" type="primary" @click="handleSearch">
搜索
</Button>
</Space>
</div>
</Card>
<Card>
<div class="selection-bar">
<Checkbox
:checked="allChecked"
:indeterminate="indeterminate"
@change="(event) => toggleAll(!!event.target.checked)"
>
全选
</Checkbox>
<span class="selection-text">
已勾选 <strong>{{ selectedKeys.length }}</strong>
</span>
<Button danger @click="handleDeleteSelected">删除</Button>
</div>
<Spin :spinning="loading">
<Empty v-if="list.length === 0" description="暂无动态" />
<Row v-else :gutter="[16, 16]">
<Col
v-for="record in list"
:key="getRowKey(record)"
:lg="8"
:md="12"
:sm="24"
:xs="24"
>
<Card class="dynamic-card">
<div class="card-header">
<Checkbox
:checked="selectedKeys.includes(getRowKey(record))"
@change="() => toggleRecord(record)"
>
选中
</Checkbox>
<Tag v-if="record.top === true" color="magenta">
置顶权重{{ record.weights || 0 }}
</Tag>
</div>
<div class="content-block">
<div class="content-label">动态内容</div>
<div class="content-text">{{ record.content || '-' }}</div>
</div>
<div v-if="getPictures(record).length > 0" class="picture-row">
<div
v-for="item in getPictures(record)"
:key="item"
class="picture-item"
>
<Image :src="item" class="picture-image" />
</div>
</div>
<div class="user-row">
<img
:src="getUserAvatar(record) || 'https://dummyimage.com/48x48/e2e8f0/64748b&text=U'"
alt=""
class="user-avatar"
>
<div class="user-meta">
<Button class="user-link" type="link" @click="openUserDetails(record)">
{{ getUserName(record) }}
</Button>
<div class="meta-line">{{ getUserAccount(record) }}</div>
<div class="meta-line">
性别{{ record.userSexName || '-' }} / 年龄{{ record.userBaseInfo?.age || '-' }}
</div>
<div class="meta-line">创建时间{{ formatDate(record.createTime) }}</div>
</div>
</div>
<div class="card-actions">
<Button size="small" type="link" @click="openAccountHandle(record)">
账号处理
</Button>
<Button size="small" type="link" @click="openTopEdit(record)">
置顶
</Button>
<Button
v-if="record.top === true"
size="small"
type="link"
@click="handleCloseTop(record)"
>
取消置顶
</Button>
</div>
</Card>
</Col>
</Row>
</Spin>
<div class="pagination">
<Pagination
:current="query.cursor"
:page-size="query.limit"
:total="total"
show-size-changer
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
</Card>
</div>
<Modal
:confirm-loading="topSaving"
:open="topModalOpen"
destroy-on-close
title="编辑置顶动态"
@cancel="topModalOpen = false"
@ok="handleSubmitTop"
>
<div class="top-form">
<div class="top-row">
<span>平台</span>
<Tag>{{ query.sysOrigin }}</Tag>
</div>
<div class="top-row top-input">
<span>权重</span>
<InputNumber
v-model:value="topForm.weights"
:min="0"
placeholder="数字越大越排在前面"
style="width: 100%"
/>
</div>
</div>
</Modal>
<AccountHandleModal
:open="accountHandleOpen"
:user-id="activeUserId"
@close="accountHandleOpen = false"
@success="loadData()"
/>
</Page>
</template>
<style scoped>
.page-grid {
display: grid;
gap: 16px;
}
.toolbar {
min-height: 40px;
}
.selection-bar {
align-items: center;
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.selection-text {
color: #64748b;
}
.dynamic-card {
height: 100%;
}
.card-header {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.content-block {
margin-bottom: 14px;
}
.content-label {
color: #2563eb;
font-weight: 600;
margin-bottom: 6px;
}
.content-text {
color: #334155;
line-height: 1.7;
min-height: 48px;
overflow-wrap: anywhere;
}
.picture-row {
display: flex;
gap: 8px;
margin-bottom: 14px;
overflow-x: auto;
}
.picture-item {
flex: 0 0 auto;
}
.picture-image {
border-radius: 12px;
height: 220px;
object-fit: cover;
width: 180px;
}
.user-row {
align-items: flex-start;
display: flex;
gap: 12px;
}
.user-avatar {
border-radius: 999px;
height: 48px;
object-fit: cover;
width: 48px;
}
.user-meta {
min-width: 0;
}
.user-link {
padding-left: 0;
}
.meta-line {
color: #64748b;
line-height: 1.8;
overflow-wrap: anywhere;
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 12px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.top-form {
display: grid;
gap: 12px;
}
.top-row {
align-items: center;
display: flex;
gap: 12px;
}
.top-input {
align-items: stretch;
flex-direction: column;
}
</style>