完成设置模块当中的配送员管理
This commit is contained in:
@@ -88,6 +88,19 @@
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<view class="table-body">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="empty-state-row">
|
||||
<text class="empty-text">加载中...</text>
|
||||
</view>
|
||||
<!-- 错误状态 -->
|
||||
<view v-else-if="fetchError !== ''" class="empty-state-row">
|
||||
<text class="empty-text">{{ fetchError }}</text>
|
||||
<button class="btn small" @click="fetchUsers">重试</button>
|
||||
</view>
|
||||
<!-- 无数据 -->
|
||||
<view v-else-if="pagedList.length === 0" class="empty-state-row">
|
||||
<text class="empty-text">暂无用户数据</text>
|
||||
</view>
|
||||
<view v-for="user in pagedList" :key="user.id" class="table-row"
|
||||
:style="{ zIndex: activeDropdownId === user.id ? 1000 : 1 }"
|
||||
>
|
||||
@@ -161,37 +174,145 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
import { supabase } from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
const activeTab = ref(0)
|
||||
// ========== MOCK DATA START ==========
|
||||
const tabs = ['全部', '微信公众号', '微信小程序', 'H5', 'PC', 'APP']
|
||||
const isAllChecked = ref(false)
|
||||
const activeDropdownId = ref<string | null>(null)
|
||||
|
||||
const userList = ref([
|
||||
{ id: '77414', avatar: '/static/logo.png', nickname: '199****0268', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '199****0268', userType: '公众号', balance: '88888.00', checked: false },
|
||||
{ id: '75311', avatar: '/static/logo.png', nickname: 'wljbhg', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100002.00', checked: false },
|
||||
{ id: '75305', avatar: '/static/logo.png', nickname: '相见欢', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75296', avatar: '/static/logo.png', nickname: '..', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75293', avatar: '/static/logo.png', nickname: '钟(钏)华', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75289', avatar: '/static/logo.png', nickname: '小二上酒', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75257', avatar: '/static/logo.png', nickname: '5+7', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75226', avatar: '/static/logo.png', nickname: '慢步前行', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75211', avatar: '/static/logo.png', nickname: '难得糊涂', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75100', avatar: '/static/logo.png', nickname: '山河远阔', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '138****5566', userType: '小程序', balance: '9800.00', checked: false },
|
||||
{ id: '74988', avatar: '/static/logo.png', nickname: '野草菓芙', isMember: '是', level: '金等', group: 'B类客户', spreadLevel: '一级', phone: '155****7788', userType: '公众号', balance: '25600.00', checked: false },
|
||||
{ id: '74833', avatar: '/static/logo.png', nickname: '星花雨月', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '', userType: 'H5', balance: '320.00', checked: false },
|
||||
{ id: '74701', avatar: '/static/logo.png', nickname: '天道酬勤', isMember: '是', level: '银等', group: 'A类客户', spreadLevel: '二级', phone: '186****3344', userType: '小程序', balance: '7700.00', checked: false },
|
||||
{ id: '74590', avatar: '/static/logo.png', nickname: '南风知劲草', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '177****1122', userType: 'APP', balance: '150.00', checked: false },
|
||||
{ id: '74422', avatar: '/static/logo.png', nickname: '大漠孤烟', isMember: '是', level: '金等', group: 'A类客户', spreadLevel: '一级', phone: '139****9900', userType: '公众号', balance: '88800.00', checked: false },
|
||||
{ id: '74310', avatar: '/static/logo.png', nickname: '小桥流水', isMember: '否', level: '无', group: 'B类客户', spreadLevel: '', phone: '', userType: 'PC', balance: '600.00', checked: false },
|
||||
{ id: '74198', avatar: '/static/logo.png', nickname: '日暗香残', isMember: '是', level: '银等', group: '无', spreadLevel: '一级', phone: '135****6677', userType: '小程序', balance: '4300.00', checked: false },
|
||||
{ id: '74056', avatar: '/static/logo.png', nickname: '梦里花开', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '', userType: '公众号', balance: '200.00', checked: false },
|
||||
{ id: '73900', avatar: '/static/logo.png', nickname: '春风十里', isMember: '是', level: '金等', group: 'A类客户', spreadLevel: '二级', phone: '151****4455', userType: 'APP', balance: '56000.00', checked: false }
|
||||
])
|
||||
// ========== MOCK DATA END ==========
|
||||
// ========== 用户展示行类型 ==========
|
||||
type UserRow = {
|
||||
id: string
|
||||
avatar: string
|
||||
nickname: string
|
||||
isMember: string
|
||||
level: string
|
||||
group: string
|
||||
spreadLevel: string
|
||||
phone: string
|
||||
userType: string
|
||||
balance: string
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
// ========== 数据状态 ==========
|
||||
const loading = ref(false)
|
||||
const fetchError = ref('')
|
||||
const userList = ref<UserRow[]>([])
|
||||
|
||||
// ========== 字段映射函数 ==========
|
||||
function formatLevelDisplay(levelNum: number | null): string {
|
||||
if (levelNum == null || levelNum <= 1) return '无'
|
||||
return 'Lv.' + levelNum
|
||||
}
|
||||
|
||||
function formatUserType(regSource: string | null, role: string | null): string {
|
||||
if (regSource === 'web') return 'PC/H5'
|
||||
if (regSource === 'mobile') return 'APP'
|
||||
if (regSource === 'wechat' || regSource === 'weixin') return '微信'
|
||||
if (role === 'admin') return '管理员'
|
||||
if (role === 'merchant') return '商家'
|
||||
if (role === 'delivery') return '配送员'
|
||||
return '用户'
|
||||
}
|
||||
|
||||
function mapDbRow(row: UTSJSONObject): UserRow {
|
||||
const id = row.getString('id') ?? ''
|
||||
const avatarUrl = row.getString('avatar_url') ?? ''
|
||||
const username = row.getString('username') ?? ''
|
||||
const emailStr = row.getString('email') ?? ''
|
||||
// 昵称:优先 username,兜底取邮箱前缀,再兜底 '—'
|
||||
let nickname = '—'
|
||||
if (username !== '') {
|
||||
nickname = username
|
||||
} else if (emailStr !== '') {
|
||||
const atIdx = emailStr.indexOf('@')
|
||||
nickname = atIdx > 0 ? emailStr.substring(0, atIdx) : emailStr
|
||||
}
|
||||
const phone = row.getString('phone') ?? ''
|
||||
const regSource = row.getString('registration_source')
|
||||
const role = row.getString('role')
|
||||
const levelNum = row.getNumber('user_level')
|
||||
const avatarFinal = avatarUrl !== '' ? avatarUrl : '/static/logo.png'
|
||||
return {
|
||||
id: id,
|
||||
avatar: avatarFinal,
|
||||
nickname: nickname,
|
||||
isMember: '—', // ak_users 无付费会员字段
|
||||
level: formatLevelDisplay(levelNum),
|
||||
group: '—', // ak_users 无分组字段
|
||||
spreadLevel: '—', // ak_users 无分销等级字段
|
||||
phone: phone,
|
||||
userType: formatUserType(regSource, role),
|
||||
balance: '—', // ak_users 无余额字段(total_spent 为消费额,语义不同)
|
||||
checked: false
|
||||
} as UserRow
|
||||
}
|
||||
|
||||
// ========== 数据请求 ==========
|
||||
// ⚠️ 前置条件:需在 Supabase 添加 admin 读取全部用户的 RLS 策略:
|
||||
// CREATE POLICY "ak_users_admin_read_all" ON public.ak_users
|
||||
// FOR SELECT TO authenticated
|
||||
// USING ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' OR auth.uid() = id);
|
||||
//
|
||||
// ✅ 使用 limit+offset URL 参数代替 Range 头,彻底避免 PostgREST 416 问题
|
||||
const fetchUsers = async (page: number = 1, ps: number = pageSize.value) => {
|
||||
if (loading.value) return // 防止并发重复请求
|
||||
loading.value = true
|
||||
fetchError.value = ''
|
||||
try {
|
||||
// offset 注入到 filter 字符串,PostgREST 将其识别为 SQL OFFSET,不发 Range 头
|
||||
const offset = (page - 1) * ps
|
||||
const offsetFilter = offset > 0 ? `offset=${offset}` : null
|
||||
const res = await supabase.select(
|
||||
'ak_users',
|
||||
offsetFilter,
|
||||
{
|
||||
columns: 'id, username, email, phone, avatar_url, role, registration_source, user_level, created_at',
|
||||
limit: ps,
|
||||
order: 'created_at.desc',
|
||||
count: 'exact' // 触发 Prefer: count=exact → 响应带 Content-Range 总行数
|
||||
}
|
||||
)
|
||||
if (res.status >= 200 && res.status < 300 && res.data != null) {
|
||||
userList.value = (res.data as UTSJSONObject[]).map((row: UTSJSONObject): UserRow => mapDbRow(row))
|
||||
// 从 Content-Range 响应头解析真实总行数(格式:0-14/total)
|
||||
let totalCount = 0
|
||||
const hdrs = res.headers
|
||||
if (hdrs != null) {
|
||||
let cr: string | null = null
|
||||
if (typeof (hdrs as any).get === 'function') {
|
||||
cr = (hdrs as any).get('content-range') as string | null
|
||||
}
|
||||
if (cr == null) {
|
||||
cr = (hdrs as UTSJSONObject)['content-range'] as string | null
|
||||
}
|
||||
if (cr != null) {
|
||||
const m = /\/(\d+)$/.exec(cr)
|
||||
if (m != null) totalCount = parseInt(m[1] ?? '0')
|
||||
}
|
||||
}
|
||||
// content-range 解析失败时:以 offset + 当前页条数 作保守兜底
|
||||
if (totalCount === 0) {
|
||||
totalCount = offset + (Array.isArray(res.data) ? (res.data as UTSJSONObject[]).length : 0)
|
||||
}
|
||||
total.value = totalCount
|
||||
} else {
|
||||
fetchError.value = '加载用户列表失败,请检查网络或 RLS 权限配置'
|
||||
}
|
||||
} catch (e) {
|
||||
fetchError.value = '请求异常,请稍后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsers(1, pageSize.value)
|
||||
})
|
||||
|
||||
// ========== PAGINATION STATE ==========
|
||||
const currentPage = ref(1)
|
||||
@@ -200,12 +321,9 @@ const jumpPageInput = ref('')
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => { const idx = pageSizeOptions.indexOf(pageSize.value); return idx >= 0 ? idx : 0 })
|
||||
const total = computed(() => userList.value.length)
|
||||
const total = ref(0) // 来自服务端 content-range 真实总行数
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const pagedList = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return userList.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
const pagedList = computed(() => userList.value) // 服务端已按页返回,直接展示当前页数据
|
||||
const visiblePages = computed((): number[] => {
|
||||
const t = totalPage.value; const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
@@ -213,15 +331,23 @@ const visiblePages = computed((): number[] => {
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageChange = (p: number) => {
|
||||
if (p < 1 || p > totalPage.value) return
|
||||
currentPage.value = p
|
||||
fetchUsers(p, pageSize.value)
|
||||
}
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
fetchUsers(1, pageSize.value)
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput.value)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) {
|
||||
currentPage.value = p
|
||||
fetchUsers(p, pageSize.value)
|
||||
}
|
||||
}
|
||||
// ========== END PAGINATION STATE ==========
|
||||
|
||||
@@ -572,4 +698,20 @@ function onDetail(user: any) {
|
||||
}
|
||||
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
.empty-state-row {
|
||||
padding: 40px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user