148 lines
4.8 KiB
Vue
148 lines
4.8 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
/**
|
|||
|
|
* 用户列表:服务端分页 + 角色/状态筛选。
|
|||
|
|
*/
|
|||
|
|
import { ref } from 'vue'
|
|||
|
|
import { onReachBottom, onShow } from '@dcloudio/uni-app'
|
|||
|
|
import { useDialog, useToast } from '@wot-ui/ui'
|
|||
|
|
import AppNavbar from '@/components/layout/AppNavbar.vue'
|
|||
|
|
import AppPageShell from '@/components/layout/AppPageShell.vue'
|
|||
|
|
import AdminFilterBar from '../../components/AdminFilterBar.vue'
|
|||
|
|
import AdminStatusBadge from '../../components/AdminStatusBadge.vue'
|
|||
|
|
import { useAdminGuard } from '../../composables/useAdminGuard'
|
|||
|
|
import { adminDelete, adminList, normalizePagination, type User } from '@/api'
|
|||
|
|
import { resolveMediaUrl } from '@/utils/request'
|
|||
|
|
|
|||
|
|
const toast = useToast()
|
|||
|
|
const dialog = useDialog()
|
|||
|
|
const { guardAdmin } = useAdminGuard()
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const list = ref<User[]>([])
|
|||
|
|
const keyword = ref('')
|
|||
|
|
const filters = ref<Record<string, unknown>>({})
|
|||
|
|
const page = ref(1)
|
|||
|
|
const finished = ref(false)
|
|||
|
|
|
|||
|
|
const filterDefs = [
|
|||
|
|
{ key: 'keyword', label: '关键词', type: 'text' as const },
|
|||
|
|
{
|
|||
|
|
key: 'role',
|
|||
|
|
label: '角色',
|
|||
|
|
type: 'select' as const,
|
|||
|
|
options: [
|
|||
|
|
{ label: '全部', value: '' },
|
|||
|
|
{ label: 'admin', value: 'admin' },
|
|||
|
|
{ label: 'editor', value: 'editor' },
|
|||
|
|
{ label: 'viewer', value: 'viewer' },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'isActive',
|
|||
|
|
label: '状态',
|
|||
|
|
type: 'select' as const,
|
|||
|
|
options: [
|
|||
|
|
{ label: '全部', value: '' },
|
|||
|
|
{ label: '启用', value: 1 },
|
|||
|
|
{ label: '停用', value: 0 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
async function load(reset = true) {
|
|||
|
|
if (!guardAdmin()) return
|
|||
|
|
if (reset) {
|
|||
|
|
page.value = 1
|
|||
|
|
finished.value = false
|
|||
|
|
list.value = []
|
|||
|
|
}
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
const data = await adminList('users', {
|
|||
|
|
page: page.value,
|
|||
|
|
pageSize: 10,
|
|||
|
|
keyword: keyword.value || undefined,
|
|||
|
|
...filters.value,
|
|||
|
|
})
|
|||
|
|
const p = normalizePagination<User>(data, page.value, 10)
|
|||
|
|
list.value = reset ? p.list : [...list.value, ...p.list]
|
|||
|
|
finished.value = list.value.length >= p.total
|
|||
|
|
}
|
|||
|
|
catch (e: unknown) {
|
|||
|
|
toast.error(e instanceof Error ? e.message : '加载失败')
|
|||
|
|
}
|
|||
|
|
finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onShow(() => load(true))
|
|||
|
|
onReachBottom(() => {
|
|||
|
|
if (finished.value || loading.value) return
|
|||
|
|
page.value += 1
|
|||
|
|
load(false)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function goCreate() {
|
|||
|
|
uni.navigateTo({ url: '/subPackages/admin/pages/users/form?mode=create' })
|
|||
|
|
}
|
|||
|
|
function goEdit(u: User) {
|
|||
|
|
uni.navigateTo({ url: `/subPackages/admin/pages/users/form?mode=edit&id=${u.id}` })
|
|||
|
|
}
|
|||
|
|
function onDelete(u: User) {
|
|||
|
|
dialog.confirm({ title: '确认删除', msg: `确定删除用户「${u.username}」吗?` }).then(async () => {
|
|||
|
|
try {
|
|||
|
|
await adminDelete('users', u.id)
|
|||
|
|
toast.success('已删除')
|
|||
|
|
load(true)
|
|||
|
|
}
|
|||
|
|
catch (e: unknown) {
|
|||
|
|
toast.error(e instanceof Error ? e.message : '删除失败')
|
|||
|
|
}
|
|||
|
|
}).catch(() => {})
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<AppPageShell admin>
|
|||
|
|
<AppNavbar title="用户管理" show-back />
|
|||
|
|
<view class="page-pad">
|
|||
|
|
<AdminFilterBar
|
|||
|
|
v-model:keyword="keyword"
|
|||
|
|
v-model="filters"
|
|||
|
|
:filters="filterDefs"
|
|||
|
|
@search="load(true)"
|
|||
|
|
@reset="load(true)"
|
|||
|
|
/>
|
|||
|
|
<view v-for="item in list" :key="item.id" class="admin-card row" @click="goEdit(item)">
|
|||
|
|
<image class="avatar" :src="resolveMediaUrl(item.avatar) || '/static/logo.svg'" mode="aspectFill" />
|
|||
|
|
<view class="main">
|
|||
|
|
<view class="title-row">
|
|||
|
|
<text class="title">{{ item.username }}</text>
|
|||
|
|
<AdminStatusBadge :value="item.isActive" :map="{ '1': '启用', '0': '停用', true: '启用', false: '停用' }" />
|
|||
|
|
</view>
|
|||
|
|
<text class="sub text-art-muted">{{ item.email }} · {{ item.role }}</text>
|
|||
|
|
</view>
|
|||
|
|
<wd-button size="small" type="warning" plain @click.stop="onDelete(item)">删除</wd-button>
|
|||
|
|
</view>
|
|||
|
|
<wd-loading v-if="loading" />
|
|||
|
|
<wd-empty v-if="!loading && !list.length" description="暂无用户" />
|
|||
|
|
</view>
|
|||
|
|
<view class="fab" @click="goCreate"><wd-icon name="plus" size="24px" color="#fff" /></view>
|
|||
|
|
</AppPageShell>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.page-pad { padding: 24rpx 32rpx 120rpx; }
|
|||
|
|
.row { padding: 24rpx 28rpx; margin-bottom: 16rpx; display: flex; align-items: center; gap: 16rpx; }
|
|||
|
|
.avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(255,255,255,0.06); }
|
|||
|
|
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
|
|||
|
|
.title-row { display: flex; align-items: center; gap: 12rpx; }
|
|||
|
|
.title { flex: 1; font-size: 30rpx; color: rgb(var(--art-text)); }
|
|||
|
|
.sub { font-size: 24rpx; }
|
|||
|
|
.fab {
|
|||
|
|
position: fixed; right: 40rpx; bottom: calc(40rpx + env(safe-area-inset-bottom));
|
|||
|
|
width: 96rpx; height: 96rpx; border-radius: 50%;
|
|||
|
|
background: rgb(var(--art-accent)); display: flex; align-items: center; justify-content: center; z-index: 100;
|
|||
|
|
}
|
|||
|
|
</style>
|