Files
ReelShortWeb/src/views/userManagement/composables/useAppUsers.js
T
Vicky bedae02946 feat: phase1 UI prep — 后台 App 用户列表支持系统筛选 + 国家中文名 + 标签语义优化
新增: 系统下拉 (ANDROID/IOS)、国家中文名映射、用户列昵称/UID 冒号标签
详情抽屉: 同步系统/国家/标签展示
2026-10-27 15:45:23 +08:00

282 lines
7.3 KiB
JavaScript

// Author: fruit.com.cn
import { onMounted, reactive, ref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getAppUserApi,
listAppUserBanRecordsApi,
listAppUserWatchHistoryApi,
pageAppUsersApi,
updateAppUserStatusApi
} from '@/api/admin/appUser'
/** 账号状态与展示文案,与后端 AppUserStatus 保持一致 */
export const APP_USER_STATUS = [
{ value: 'normal', label: '正常', tag: 'success' },
{ value: 'limited', label: '受限', tag: 'warning' },
{ value: 'banned', label: '封禁', tag: 'danger' },
{ value: 'deactivated', label: '已注销', tag: 'info' }
]
export const COUNTRY_OPTIONS = [
{ value: 'ID', label: '印度尼西亚' },
{ value: 'SG', label: '新加坡' },
{ value: 'MY', label: '马来西亚' },
{ value: 'PH', label: '菲律宾' },
{ value: 'TH', label: '泰国' },
{ value: 'VN', label: '越南' },
{ value: 'CN', label: '中国' }
]
/** 国家码 → 中文名,与后端 CountryProfile 保持一致;未收录的仍显示原码 */
const COUNTRY_TEXT = {
ID: '印度尼西亚',
MY: '马来西亚',
SG: '新加坡',
TH: '泰国',
VN: '越南',
PH: '菲律宾',
KH: '柬埔寨',
MM: '缅甸',
LA: '老挝',
BN: '文莱',
CN: '中国',
TW: '中国台湾',
HK: '中国香港',
MO: '中国澳门',
JP: '日本',
KR: '韩国',
IN: '印度',
PK: '巴基斯坦',
BD: '孟加拉国',
LK: '斯里兰卡',
NP: '尼泊尔',
US: '美国',
CA: '加拿大',
MX: '墨西哥',
BR: '巴西',
AR: '阿根廷',
CL: '智利',
CO: '哥伦比亚',
PE: '秘鲁',
VE: '委内瑞拉',
EC: '厄瓜多尔',
GT: '危地马拉',
DO: '多米尼加',
GB: '英国',
DE: '德国',
FR: '法国',
ES: '西班牙',
IT: '意大利',
NL: '荷兰',
PL: '波兰',
RU: '俄罗斯',
UA: '乌克兰',
TR: '土耳其',
SA: '沙特阿拉伯',
AE: '阿联酋',
IL: '以色列',
EG: '埃及',
NG: '尼日利亚',
ZA: '南非',
KE: '肯尼亚',
MA: '摩洛哥',
AU: '澳大利亚',
NZ: '新西兰'
}
/** 国家列统一展示中文名,无值时显示横线 */
export function countryText(code) {
const value = (code || '').toUpperCase()
return COUNTRY_TEXT[value] || value || '—'
}
export function statusMeta(status) {
return (
APP_USER_STATUS.find((item) => item.value === status) || { label: status || '-', tag: 'info' }
)
}
const PLATFORM_TEXT = { ANDROID: 'Android', IOS: 'iOS', WEB: 'Web' }
/** 客户端平台,只有 iOS / Android 两种取值(缺记录显示横线)。 */
export function platformText(row) {
const platform = row?.platform || ''
return PLATFORM_TEXT[platform] || platform || '—'
}
/** 后端返回 ISO 时间串,统一格式化成后台可读的本地时间 */
export function formatDateTime(value) {
return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'
}
/** 毫秒转 mm:ss,观看流水里的进度用 */
export function formatDuration(ms) {
const total = Math.max(0, Math.floor((ms || 0) / 1000))
const minute = String(Math.floor(total / 60)).padStart(2, '0')
const second = String(total % 60).padStart(2, '0')
return `${minute}:${second}`
}
export function useAppUsers() {
const users = ref([])
const total = ref(0)
const loading = ref(false)
const keyword = ref('')
const statusFilter = ref(undefined)
const countryFilter = ref(undefined)
const platformFilter = ref(undefined)
const currentPage = ref(1)
const pageSize = ref(10)
const detailVisible = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const banRecords = ref([])
const watchHistory = ref([])
const statusDialogVisible = ref(false)
const statusSaving = ref(false)
const statusTarget = ref(null)
const statusForm = reactive({ status: 'banned', reason: '' })
async function load() {
loading.value = true
try {
const res = await pageAppUsersApi({
pageIndex: currentPage.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
status: statusFilter.value,
countryCode: countryFilter.value,
platform: platformFilter.value || undefined
})
users.value = res.data?.list || []
total.value = res.data?.total || 0
} finally {
loading.value = false
}
}
function queryUsers() {
currentPage.value = 1
load()
}
function resetFilters() {
keyword.value = ''
statusFilter.value = undefined
countryFilter.value = undefined
platformFilter.value = undefined
queryUsers()
}
function handleSizeChange() {
currentPage.value = 1
load()
}
const detailTabs = reactive({ active: 'watch' })
async function openDetail(row) {
detailVisible.value = true
detailLoading.value = true
detail.value = row
banRecords.value = []
watchHistory.value = []
detailTabs.active = 'watch'
try {
// 三个请求互不依赖,并发拉取,抽屉打开更快
const [profile, bans, history] = await Promise.all([
getAppUserApi(row.id),
listAppUserBanRecordsApi(row.id),
listAppUserWatchHistoryApi(row.id, 50)
])
detail.value = profile.data || row
banRecords.value = bans.data || []
watchHistory.value = history.data || []
} finally {
detailLoading.value = false
}
}
function openStatusDialog(row, preset = 'banned') {
statusTarget.value = row
statusForm.status = preset
statusForm.reason = ''
statusDialogVisible.value = true
}
async function confirmStatus() {
const row = statusTarget.value
if (!row) return
const label = statusMeta(statusForm.status).label
statusSaving.value = true
try {
await updateAppUserStatusApi(row.id, statusForm.status, statusForm.reason.trim() || undefined)
ElMessage.success(`已${label},该用户全部登录会话已失效`)
statusDialogVisible.value = false
await load()
if (detailVisible.value && detail.value?.id === row.id) {
detail.value = { ...detail.value, status: statusForm.status }
banRecords.value = (await listAppUserBanRecordsApi(row.id)).data || []
}
} finally {
statusSaving.value = false
}
}
/** 恢复正常:不需要填原因,但仍要二次确认,避免误点 */
async function restoreUser(row) {
try {
await ElMessageBox.confirm(
`确认把「${row.nickname || row.uid}」恢复为正常账号?`,
'恢复正常',
{ confirmButtonText: '确认恢复', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
await updateAppUserStatusApi(row.id, 'normal')
ElMessage.success('账号已恢复正常')
await load()
if (detailVisible.value && detail.value?.id === row.id) {
detail.value = { ...detail.value, status: 'normal' }
}
}
onMounted(load)
return {
users,
total,
loading,
keyword,
statusFilter,
countryFilter,
platformFilter,
currentPage,
pageSize,
detailVisible,
detailLoading,
detail,
banRecords,
watchHistory,
detailTabs,
statusDialogVisible,
statusSaving,
statusTarget,
statusForm,
load,
queryUsers,
resetFilters,
handleSizeChange,
openDetail,
openStatusDialog,
confirmStatus,
restoreUser
}
}