完成设置模块当中的配送员管理

This commit is contained in:
2026-03-18 16:12:37 +08:00
parent b7c8881e55
commit f1a6c18dfb
8 changed files with 2603 additions and 260 deletions

View File

@@ -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>