chore(merge): 合并 phase1 UI prep 后台用户系统筛选与列表展示
合并后新增收藏/点赞列表页、用户单元格组件与系统筛选, 并统一观看时间格式与标签文案。
This commit is contained in:
commit
b68361b037
16 files changed
+685
-39
No files matched your search
@@ -11,6 +11,8 @@ export interface AppUserPageParams {
|
|||||||
status?: string
|
status?: string
|
||||||
countryCode?: string
|
countryCode?: string
|
||||||
registerSource?: string
|
registerSource?: string
|
||||||
|
/** 客户端平台:ANDROID / IOS / WEB */
|
||||||
|
platform?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WatchHistoryPageParams {
|
export interface WatchHistoryPageParams {
|
||||||
@@ -21,6 +23,8 @@ export interface WatchHistoryPageParams {
|
|||||||
dramaTitle?: string
|
dramaTitle?: string
|
||||||
dramaId?: number
|
dramaId?: number
|
||||||
episodeId?: number
|
episodeId?: number
|
||||||
|
/** 客户端平台:ANDROID / IOS / WEB */
|
||||||
|
platform?: string
|
||||||
source?: string
|
source?: string
|
||||||
finished?: boolean
|
finished?: boolean
|
||||||
startTime?: string
|
startTime?: string
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
|
/** 收藏与点赞只读分页筛选条件。 */
|
||||||
|
export interface EngagementPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
userId?: string | number
|
||||||
|
deviceId?: string
|
||||||
|
/** 客户端平台:ANDROID / IOS / WEB */
|
||||||
|
platform?: string
|
||||||
|
dramaId?: string | number
|
||||||
|
dramaTitle?: string
|
||||||
|
targetType?: string
|
||||||
|
targetId?: string | number
|
||||||
|
episodeId?: string | number
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后台互动列表行。 */
|
||||||
|
export interface EngagementItem {
|
||||||
|
id: string
|
||||||
|
userId: string | null
|
||||||
|
uid: string | null
|
||||||
|
nickname: string | null
|
||||||
|
deviceId: string | null
|
||||||
|
/** 客户端平台 ANDROID / IOS / WEB;老客户端未上报时为空。 */
|
||||||
|
platform: string | null
|
||||||
|
anonymous: boolean
|
||||||
|
dramaId: string | null
|
||||||
|
dramaTitle: string | null
|
||||||
|
dramaCover: string | null
|
||||||
|
dramaStatus: string | null
|
||||||
|
dramaPublished: boolean | null
|
||||||
|
targetType: string
|
||||||
|
targetId: string
|
||||||
|
episodeId: string | null
|
||||||
|
episodeNo: number | null
|
||||||
|
episodeTitle: string | null
|
||||||
|
commentSummary: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询收藏分页。 */
|
||||||
|
export const pageFavoritesApi = (params: EngagementPageParams) =>
|
||||||
|
request.get<PageResult<EngagementItem>>({ url: '/admin/favorites/page', params })
|
||||||
|
|
||||||
|
/** 查询点赞分页。 */
|
||||||
|
export const pageLikesApi = (params: EngagementPageParams) =>
|
||||||
|
request.get<PageResult<EngagementItem>>({ url: '/admin/likes/page', params })
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-user-toolbar .app-user-status,
|
.app-user-toolbar .app-user-status,
|
||||||
.app-user-toolbar .app-user-country {
|
.app-user-toolbar .app-user-country,
|
||||||
|
.app-user-toolbar .app-user-platform {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
flex: 0 0 150px;
|
flex: 0 0 150px;
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
.app-user-meta {
|
.app-user-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台各列表共用的「用户」单元格。
|
||||||
|
*
|
||||||
|
* 口径:已登录显示昵称 + `UID xxx`;未登录显示「游客」标签 + `设备 xxx`。
|
||||||
|
* 两行都带标签,避免一串随机字母看不出是什么。平台由独立的「系统」列展示。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
row: { type: Object, required: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
const isGuest = computed(() => props.row?.userId == null)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-user-meta">
|
||||||
|
<template v-if="isGuest">
|
||||||
|
<el-tag type="info" size="small">游客</el-tag>
|
||||||
|
<span class="app-user-uid app-user-device" :title="row.deviceId || ''">
|
||||||
|
设备: {{ row.deviceId || '—' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="app-user-name" :title="row.nickname || ''">
|
||||||
|
昵称: {{ row.nickname || '未设置昵称' }}
|
||||||
|
</span>
|
||||||
|
<span class="app-user-uid">UID: {{ row.uid || `#${row.userId}` }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-user-device {
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,15 +13,21 @@ const props = defineProps({
|
|||||||
showLanguages: { type: Boolean, default: true },
|
showLanguages: { type: Boolean, default: true },
|
||||||
required: { type: Boolean, default: true }
|
required: { type: Boolean, default: true }
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:languages'])
|
const emit = defineEmits(['update:languages', 'update:locales'])
|
||||||
|
|
||||||
function changeLanguages(codes) {
|
function changeLanguages(codes) {
|
||||||
const next = codes.includes('zh') ? codes : ['zh', ...codes]
|
const next = codes.includes('zh') ? codes : ['zh', ...codes]
|
||||||
|
const locales = { ...props.locales }
|
||||||
next.forEach((code) => {
|
next.forEach((code) => {
|
||||||
if (props.locales[code] == null) props.locales[code] = ''
|
if (locales[code] == null) locales[code] = ''
|
||||||
})
|
})
|
||||||
|
emit('update:locales', locales)
|
||||||
emit('update:languages', next)
|
emit('update:languages', next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeLocale(code, value) {
|
||||||
|
emit('update:locales', { ...props.locales, [code]: value })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -50,13 +56,15 @@ function changeLanguages(codes) {
|
|||||||
>
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-if="!multiline"
|
v-if="!multiline"
|
||||||
v-model="locales[code]"
|
:model-value="locales[code]"
|
||||||
|
@update:model-value="changeLocale(code, $event)"
|
||||||
:maxlength="maxlength"
|
:maxlength="maxlength"
|
||||||
:placeholder="'请填写' + languageLabel(code) + label"
|
:placeholder="'请填写' + languageLabel(code) + label"
|
||||||
/>
|
/>
|
||||||
<el-input
|
<el-input
|
||||||
v-else
|
v-else
|
||||||
v-model="locales[code]"
|
:model-value="locales[code]"
|
||||||
|
@update:model-value="changeLocale(code, $event)"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="2"
|
:rows="2"
|
||||||
:maxlength="maxlength"
|
:maxlength="maxlength"
|
||||||
|
|||||||
@@ -372,6 +372,7 @@ onMounted(loadClassifications)
|
|||||||
v-model:candidate-keyword="candidateKeyword"
|
v-model:candidate-keyword="candidateKeyword"
|
||||||
:editing-id="editingId"
|
:editing-id="editingId"
|
||||||
:form="form"
|
:form="form"
|
||||||
|
@update:form="Object.assign(form, $event)"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
:candidate-loading="candidateLoading"
|
:candidate-loading="candidateLoading"
|
||||||
:candidates="candidates"
|
:candidates="candidates"
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ onMounted(load)
|
|||||||
<LocaleTextFields
|
<LocaleTextFields
|
||||||
v-model:languages="languages"
|
v-model:languages="languages"
|
||||||
:locales="form.locales"
|
:locales="form.locales"
|
||||||
|
@update:locales="form.locales = $event"
|
||||||
label="名称"
|
label="名称"
|
||||||
:maxlength="64"
|
:maxlength="64"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ defineProps({
|
|||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:editorVisible',
|
'update:editorVisible',
|
||||||
'update:languages',
|
'update:languages',
|
||||||
|
'update:form',
|
||||||
'update:candidateVisible',
|
'update:candidateVisible',
|
||||||
'update:candidateKeyword',
|
'update:candidateKeyword',
|
||||||
'save',
|
'save',
|
||||||
@@ -61,13 +62,25 @@ const emit = defineEmits([
|
|||||||
label="名称"
|
label="名称"
|
||||||
:maxlength="64"
|
:maxlength="64"
|
||||||
@update:languages="emit('update:languages', $event)"
|
@update:languages="emit('update:languages', $event)"
|
||||||
|
@update:locales="emit('update:form', { ...form, locales: $event })"
|
||||||
/>
|
/>
|
||||||
<div class="classification-form-row">
|
<div class="classification-form-row">
|
||||||
<el-form-item label="排序">
|
<el-form-item label="排序">
|
||||||
<el-input-number v-model="form.sort" :min="0" :max="SORT_MAX" controls-position="right" />
|
<el-input-number
|
||||||
|
:model-value="form.sort"
|
||||||
|
:min="0"
|
||||||
|
:max="SORT_MAX"
|
||||||
|
controls-position="right"
|
||||||
|
@update:model-value="emit('update:form', { ...form, sort: $event })"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
|
<el-switch
|
||||||
|
:model-value="form.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
@update:model-value="emit('update:form', { ...form, status: $event })"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|||||||
@@ -641,6 +641,7 @@ onMounted(async () => {
|
|||||||
<LocaleTextFields
|
<LocaleTextFields
|
||||||
:languages="textLanguages"
|
:languages="textLanguages"
|
||||||
:locales="form.titleLocales"
|
:locales="form.titleLocales"
|
||||||
|
@update:locales="form.titleLocales = $event"
|
||||||
label="名称"
|
label="名称"
|
||||||
:maxlength="80"
|
:maxlength="80"
|
||||||
@update:languages="changeTextLanguages"
|
@update:languages="changeTextLanguages"
|
||||||
@@ -672,6 +673,7 @@ onMounted(async () => {
|
|||||||
<LocaleTextFields
|
<LocaleTextFields
|
||||||
:languages="textLanguages"
|
:languages="textLanguages"
|
||||||
:locales="form.synopsisLocales"
|
:locales="form.synopsisLocales"
|
||||||
|
@update:locales="form.synopsisLocales = $event"
|
||||||
label="简介"
|
label="简介"
|
||||||
:maxlength="1000"
|
:maxlength="1000"
|
||||||
multiline
|
multiline
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ onMounted(loadCategories)
|
|||||||
<LocaleTextFields
|
<LocaleTextFields
|
||||||
:languages="languages"
|
:languages="languages"
|
||||||
:locales="form.locales"
|
:locales="form.locales"
|
||||||
|
@update:locales="form.locales = $event"
|
||||||
label="名称"
|
label="名称"
|
||||||
:maxlength="64"
|
:maxlength="64"
|
||||||
@update:languages="changeLanguages"
|
@update:languages="changeLanguages"
|
||||||
@@ -439,6 +440,7 @@ onMounted(loadCategories)
|
|||||||
<LocaleTextFields
|
<LocaleTextFields
|
||||||
:languages="languages"
|
:languages="languages"
|
||||||
:locales="form.descriptionLocales"
|
:locales="form.descriptionLocales"
|
||||||
|
@update:locales="form.descriptionLocales = $event"
|
||||||
label="描述"
|
label="描述"
|
||||||
:maxlength="255"
|
:maxlength="255"
|
||||||
multiline
|
multiline
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ onMounted(load)
|
|||||||
<LocaleTextFields
|
<LocaleTextFields
|
||||||
v-model:languages="languages"
|
v-model:languages="languages"
|
||||||
:locales="form.locales"
|
:locales="form.locales"
|
||||||
|
@update:locales="form.locales = $event"
|
||||||
label="名称"
|
label="名称"
|
||||||
:maxlength="64"
|
:maxlength="64"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import { useRbac } from '@/stores/rbac'
|
|||||||
import {
|
import {
|
||||||
APP_USER_STATUS,
|
APP_USER_STATUS,
|
||||||
COUNTRY_OPTIONS,
|
COUNTRY_OPTIONS,
|
||||||
|
countryText,
|
||||||
|
formatDateTime,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
|
platformText,
|
||||||
statusMeta,
|
statusMeta,
|
||||||
systemText,
|
|
||||||
useAppUsers
|
useAppUsers
|
||||||
} from './composables/useAppUsers'
|
} from './composables/useAppUsers'
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ const {
|
|||||||
keyword,
|
keyword,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
countryFilter,
|
countryFilter,
|
||||||
|
platformFilter,
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
detailVisible,
|
detailVisible,
|
||||||
@@ -53,6 +56,11 @@ const SOURCE_TEXT = {
|
|||||||
FAVORITE: '收藏'
|
FAVORITE: '收藏'
|
||||||
}
|
}
|
||||||
const BAN_TYPE_TEXT = { BAN: '封禁', LIMIT: '限制', MUTE: '禁言' }
|
const BAN_TYPE_TEXT = { BAN: '封禁', LIMIT: '限制', MUTE: '禁言' }
|
||||||
|
const PLATFORM_OPTIONS = [
|
||||||
|
{ value: '', label: '全部系统' },
|
||||||
|
{ value: 'ANDROID', label: 'Android' },
|
||||||
|
{ value: 'IOS', label: 'iOS' }
|
||||||
|
]
|
||||||
const APPEAL_TEXT = {
|
const APPEAL_TEXT = {
|
||||||
none: '未申诉',
|
none: '未申诉',
|
||||||
pending: '待处理',
|
pending: '待处理',
|
||||||
@@ -116,6 +124,20 @@ function canBeBanned(row) {
|
|||||||
:value="item.value"
|
:value="item.value"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-select
|
||||||
|
v-model="platformFilter"
|
||||||
|
class="app-user-platform"
|
||||||
|
clearable
|
||||||
|
placeholder="全部系统"
|
||||||
|
@change="queryUsers"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in PLATFORM_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="countryFilter"
|
v-model="countryFilter"
|
||||||
class="app-user-country"
|
class="app-user-country"
|
||||||
@@ -165,24 +187,24 @@ function canBeBanned(row) {
|
|||||||
<template v-else>{{ avatarText(row) }}</template>
|
<template v-else>{{ avatarText(row) }}</template>
|
||||||
</span>
|
</span>
|
||||||
<div class="app-user-meta">
|
<div class="app-user-meta">
|
||||||
<span class="app-user-name" :title="row.nickname">{{
|
<span class="app-user-name" :title="row.nickname || ''">
|
||||||
row.nickname || '未设置昵称'
|
昵称: {{ row.nickname || '未设置昵称' }}
|
||||||
}}</span>
|
</span>
|
||||||
<span class="app-user-uid" :title="row.uid">{{ row.uid }}</span>
|
<span class="app-user-uid">UID: {{ row.uid }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="系统" width="130" show-overflow-tooltip>
|
<el-table-column label="系统" width="110" show-overflow-tooltip>
|
||||||
<template #default="{ row }">{{ systemText(row) }}</template>
|
<template #default="{ row }">{{ platformText(row) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="手机 / 邮箱" min-width="150" show-overflow-tooltip>
|
<el-table-column label="手机 / 邮箱" min-width="150" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="app-user-mono">{{ row.maskedTarget || '—' }}</span>
|
<span class="app-user-mono">{{ row.maskedTarget || '—' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="国家" width="90">
|
<el-table-column label="国家" width="110">
|
||||||
<template #default="{ row }">{{ row.countryCode || '—' }}</template>
|
<template #default="{ row }">{{ countryText(row.countryCode) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="90">
|
<el-table-column label="状态" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -248,8 +270,8 @@ function canBeBanned(row) {
|
|||||||
<template v-else>{{ avatarText(detail) }}</template>
|
<template v-else>{{ avatarText(detail) }}</template>
|
||||||
</span>
|
</span>
|
||||||
<div class="app-user-meta">
|
<div class="app-user-meta">
|
||||||
<span class="app-user-name">{{ detail.nickname || '未设置昵称' }}</span>
|
<span class="app-user-name"> 昵称: {{ detail.nickname || '未设置昵称' }} </span>
|
||||||
<span class="app-user-uid">{{ detail.uid }}</span>
|
<span class="app-user-uid">UID: {{ detail.uid }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag :type="statusMeta(detail.status).tag" effect="light" style="margin-left: auto">
|
<el-tag :type="statusMeta(detail.status).tag" effect="light" style="margin-left: auto">
|
||||||
{{ statusMeta(detail.status).label }}
|
{{ statusMeta(detail.status).label }}
|
||||||
@@ -262,7 +284,10 @@ function canBeBanned(row) {
|
|||||||
{{ detail.maskedTarget || '—' }}
|
{{ detail.maskedTarget || '—' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="国家 / 地区">
|
<el-descriptions-item label="国家 / 地区">
|
||||||
{{ detail.countryCode || '—' }} / {{ detail.regionCode || '—' }}
|
{{ countryText(detail.countryCode) }}
|
||||||
|
<template v-if="detail.regionCode && detail.regionCode !== detail.countryCode">
|
||||||
|
/ {{ countryText(detail.regionCode) }}
|
||||||
|
</template>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="语言">{{ detail.language || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="语言">{{ detail.language || '—' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="时区">{{ detail.timezone || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="时区">{{ detail.timezone || '—' }}</el-descriptions-item>
|
||||||
@@ -272,7 +297,7 @@ function canBeBanned(row) {
|
|||||||
<el-descriptions-item label="最后登录">{{
|
<el-descriptions-item label="最后登录">{{
|
||||||
detail.lastLoginAt || '—'
|
detail.lastLoginAt || '—'
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
<el-descriptions-item label="系统">{{ systemText(detail) }}</el-descriptions-item>
|
<el-descriptions-item label="系统">{{ platformText(detail) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="会员">
|
<el-descriptions-item label="会员">
|
||||||
{{ detail.vipLevel ? `等级 ${detail.vipLevel}` : '未开通(第二阶段)' }}
|
{{ detail.vipLevel ? `等级 ${detail.vipLevel}` : '未开通(第二阶段)' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -304,15 +329,17 @@ function canBeBanned(row) {
|
|||||||
<el-table-column label="完成" width="70">
|
<el-table-column label="完成" width="70">
|
||||||
<template #default="{ row }">{{ row.finished ? '是' : '否' }}</template>
|
<template #default="{ row }">{{ row.finished ? '是' : '否' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="系统" width="120" show-overflow-tooltip>
|
<el-table-column label="系统" width="100" show-overflow-tooltip>
|
||||||
<template #default="{ row }">{{ systemText(row) }}</template>
|
<template #default="{ row }">{{ platformText(row) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="来源" width="80">
|
<el-table-column label="来源" width="80">
|
||||||
<template #default="{ row }">{{
|
<template #default="{ row }">{{
|
||||||
SOURCE_TEXT[row.source] || row.source || '—'
|
SOURCE_TEXT[row.source] || row.source || '—'
|
||||||
}}</template>
|
}}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="watchedAt" label="时间" width="160" show-overflow-tooltip />
|
<el-table-column label="时间" width="160">
|
||||||
|
<template #default="{ row }">{{ formatDateTime(row.watchedAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import PageHeading from '@/views/components/PageHeading.vue'
|
||||||
|
import AppUserCell from '@/views/components/AppUserCell.vue'
|
||||||
|
import { pageFavoritesApi } from '@/api/admin/engagement'
|
||||||
|
import { platformText } from './composables/useAppUsers'
|
||||||
|
|
||||||
|
const PAGE_SIZES = [10, 20, 50]
|
||||||
|
const PLATFORM_OPTIONS = [
|
||||||
|
{ value: '', label: '全部系统' },
|
||||||
|
{ value: 'ANDROID', label: 'Android' },
|
||||||
|
{ value: 'IOS', label: 'iOS' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
keyword: '',
|
||||||
|
deviceId: '',
|
||||||
|
dramaTitle: '',
|
||||||
|
platform: '',
|
||||||
|
range: null
|
||||||
|
})
|
||||||
|
const rows = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const pageIndex = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await pageFavoritesApi({
|
||||||
|
pageIndex: pageIndex.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
keyword: filters.keyword.trim() || undefined,
|
||||||
|
deviceId: filters.deviceId.trim() || undefined,
|
||||||
|
dramaTitle: filters.dramaTitle.trim() || undefined,
|
||||||
|
platform: filters.platform || undefined,
|
||||||
|
startTime: filters.range?.[0] || undefined,
|
||||||
|
endTime: filters.range?.[1] || undefined
|
||||||
|
})
|
||||||
|
rows.value = res.data?.list || []
|
||||||
|
total.value = res.data?.total || 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
pageIndex.value = 1
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
Object.assign(filters, {
|
||||||
|
keyword: '',
|
||||||
|
deviceId: '',
|
||||||
|
dramaTitle: '',
|
||||||
|
platform: '',
|
||||||
|
range: null
|
||||||
|
})
|
||||||
|
search()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSizeChange() {
|
||||||
|
pageIndex.value = 1
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(value) {
|
||||||
|
return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeading eyebrow="用户管理" title="收藏列表" />
|
||||||
|
<section class="panel">
|
||||||
|
<div class="console-toolbar">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.keyword"
|
||||||
|
class="console-keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="uid / 昵称"
|
||||||
|
@keyup.enter="search"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="filters.deviceId"
|
||||||
|
class="console-keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="设备 ID"
|
||||||
|
@keyup.enter="search"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="filters.dramaTitle"
|
||||||
|
class="console-keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="短剧名称"
|
||||||
|
@keyup.enter="search"
|
||||||
|
/>
|
||||||
|
<el-select v-model="filters.platform" class="console-select" placeholder="全部系统">
|
||||||
|
<el-option
|
||||||
|
v-for="item in PLATFORM_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-date-picker
|
||||||
|
v-model="filters.range"
|
||||||
|
class="engagement-range"
|
||||||
|
type="datetimerange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="search">查询</el-button>
|
||||||
|
<el-button @click="resetFilters">重置</el-button>
|
||||||
|
<span class="console-count">共 {{ total }} 条</span>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="rows"
|
||||||
|
row-key="id"
|
||||||
|
border
|
||||||
|
class="drama-el-table"
|
||||||
|
empty-text="暂无收藏"
|
||||||
|
>
|
||||||
|
<el-table-column label="用户" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<AppUserCell :row="row" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="系统" width="100">
|
||||||
|
<template #default="{ row }">{{ platformText(row) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="短剧" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="engagement-drama">
|
||||||
|
<el-image
|
||||||
|
v-if="row.dramaCover"
|
||||||
|
:src="row.dramaCover"
|
||||||
|
fit="cover"
|
||||||
|
class="engagement-cover"
|
||||||
|
/>
|
||||||
|
<span>{{ row.dramaTitle || `短剧 #${row.dramaId || '—'}` }}</span>
|
||||||
|
<el-tag v-if="row.dramaPublished === false" type="info" size="small" effect="plain"
|
||||||
|
>未发布</el-tag
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="收藏时间" min-width="170">
|
||||||
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageIndex"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="PAGE_SIZES"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.engagement-range {
|
||||||
|
width: 360px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-drama {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-cover {
|
||||||
|
width: 40px;
|
||||||
|
height: 56px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import PageHeading from '@/views/components/PageHeading.vue'
|
||||||
|
import AppUserCell from '@/views/components/AppUserCell.vue'
|
||||||
|
import { pageLikesApi } from '@/api/admin/engagement'
|
||||||
|
import { platformText } from './composables/useAppUsers'
|
||||||
|
|
||||||
|
const PAGE_SIZES = [10, 20, 50]
|
||||||
|
const TARGET_OPTIONS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: 'DRAMA', label: '短剧' },
|
||||||
|
{ value: 'EPISODE', label: '剧集' },
|
||||||
|
{ value: 'COMMENT', label: '评论' }
|
||||||
|
]
|
||||||
|
const PLATFORM_OPTIONS = [
|
||||||
|
{ value: '', label: '全部系统' },
|
||||||
|
{ value: 'ANDROID', label: 'Android' },
|
||||||
|
{ value: 'IOS', label: 'iOS' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
keyword: '',
|
||||||
|
deviceId: '',
|
||||||
|
dramaTitle: '',
|
||||||
|
range: null,
|
||||||
|
targetType: '',
|
||||||
|
platform: ''
|
||||||
|
})
|
||||||
|
const rows = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const pageIndex = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await pageLikesApi({
|
||||||
|
pageIndex: pageIndex.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
keyword: filters.keyword.trim() || undefined,
|
||||||
|
deviceId: filters.deviceId.trim() || undefined,
|
||||||
|
dramaTitle: filters.dramaTitle.trim() || undefined,
|
||||||
|
targetType: filters.targetType || undefined,
|
||||||
|
platform: filters.platform || undefined,
|
||||||
|
startTime: filters.range?.[0] || undefined,
|
||||||
|
endTime: filters.range?.[1] || undefined
|
||||||
|
})
|
||||||
|
rows.value = res.data?.list || []
|
||||||
|
total.value = res.data?.total || 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
pageIndex.value = 1
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
Object.assign(filters, {
|
||||||
|
keyword: '',
|
||||||
|
deviceId: '',
|
||||||
|
dramaTitle: '',
|
||||||
|
range: null,
|
||||||
|
targetType: '',
|
||||||
|
platform: ''
|
||||||
|
})
|
||||||
|
search()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSizeChange() {
|
||||||
|
pageIndex.value = 1
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(value) {
|
||||||
|
return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetText(row) {
|
||||||
|
if (row.targetType === 'COMMENT') return row.commentSummary || '—'
|
||||||
|
if (row.targetType === 'EPISODE') return `${row.dramaTitle || '—'} 第 ${row.episodeNo ?? '—'} 集`
|
||||||
|
return row.dramaTitle || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeText(type) {
|
||||||
|
return TARGET_OPTIONS.find((item) => item.value === type)?.label || type || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeading eyebrow="用户管理" title="点赞列表" />
|
||||||
|
<section class="panel">
|
||||||
|
<div class="console-toolbar">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.keyword"
|
||||||
|
class="console-keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="uid / 昵称"
|
||||||
|
@keyup.enter="search"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="filters.deviceId"
|
||||||
|
class="console-keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="设备 ID"
|
||||||
|
@keyup.enter="search"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="filters.dramaTitle"
|
||||||
|
class="console-keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="短剧名称"
|
||||||
|
@keyup.enter="search"
|
||||||
|
/>
|
||||||
|
<el-select v-model="filters.targetType" class="console-select" placeholder="全部目标类型">
|
||||||
|
<el-option
|
||||||
|
v-for="item in TARGET_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="filters.platform" class="console-select" placeholder="全部系统">
|
||||||
|
<el-option
|
||||||
|
v-for="item in PLATFORM_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="filters.range"
|
||||||
|
class="engagement-range"
|
||||||
|
type="datetimerange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="search">查询</el-button>
|
||||||
|
<el-button @click="resetFilters">重置</el-button>
|
||||||
|
<span class="console-count">共 {{ total }} 条</span>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="rows"
|
||||||
|
row-key="id"
|
||||||
|
border
|
||||||
|
class="drama-el-table"
|
||||||
|
empty-text="暂无点赞"
|
||||||
|
>
|
||||||
|
<el-table-column label="用户" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<AppUserCell :row="row" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="系统" width="100">
|
||||||
|
<template #default="{ row }">{{ platformText(row) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="目标类型" width="110">
|
||||||
|
<template #default="{ row }">{{ typeText(row.targetType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="目标" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }"
|
||||||
|
><span :title="targetText(row)">{{ targetText(row) }}</span></template
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="dramaTitle" label="所属短剧" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="点赞时间" min-width="170">
|
||||||
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageIndex"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="PAGE_SIZES"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.engagement-range {
|
||||||
|
width: 360px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
import PageHeading from '@/views/components/PageHeading.vue'
|
import PageHeading from '@/views/components/PageHeading.vue'
|
||||||
|
import AppUserCell from '@/views/components/AppUserCell.vue'
|
||||||
import { pageWatchHistoryApi } from '@/api/admin/appUser'
|
import { pageWatchHistoryApi } from '@/api/admin/appUser'
|
||||||
import { formatDuration, systemText } from './composables/useAppUsers'
|
import { formatDateTime, formatDuration, platformText } from './composables/useAppUsers'
|
||||||
|
|
||||||
const SOURCE_OPTIONS = [
|
const SOURCE_OPTIONS = [
|
||||||
{ value: '', label: '全部' },
|
{ value: '', label: '全部' },
|
||||||
@@ -20,11 +21,18 @@ const FINISHED_OPTIONS = [
|
|||||||
{ value: false, label: '未看完' }
|
{ value: false, label: '未看完' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const PLATFORM_OPTIONS = [
|
||||||
|
{ value: '', label: '全部系统' },
|
||||||
|
{ value: 'ANDROID', label: 'Android' },
|
||||||
|
{ value: 'IOS', label: 'iOS' }
|
||||||
|
]
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
dramaTitle: '',
|
dramaTitle: '',
|
||||||
source: '',
|
source: '',
|
||||||
finished: '',
|
finished: '',
|
||||||
|
platform: '',
|
||||||
range: null
|
range: null
|
||||||
})
|
})
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
@@ -43,6 +51,7 @@ async function load() {
|
|||||||
dramaTitle: filters.dramaTitle.trim() || undefined,
|
dramaTitle: filters.dramaTitle.trim() || undefined,
|
||||||
source: filters.source || undefined,
|
source: filters.source || undefined,
|
||||||
finished: filters.finished === '' ? undefined : filters.finished,
|
finished: filters.finished === '' ? undefined : filters.finished,
|
||||||
|
platform: filters.platform || undefined,
|
||||||
startTime: filters.range?.[0] || undefined,
|
startTime: filters.range?.[0] || undefined,
|
||||||
endTime: filters.range?.[1] || undefined
|
endTime: filters.range?.[1] || undefined
|
||||||
})
|
})
|
||||||
@@ -63,6 +72,7 @@ function resetFilters() {
|
|||||||
filters.dramaTitle = ''
|
filters.dramaTitle = ''
|
||||||
filters.source = ''
|
filters.source = ''
|
||||||
filters.finished = ''
|
filters.finished = ''
|
||||||
|
filters.platform = ''
|
||||||
filters.range = null
|
filters.range = null
|
||||||
search()
|
search()
|
||||||
}
|
}
|
||||||
@@ -144,6 +154,19 @@ onMounted(load)
|
|||||||
:value="item.value"
|
:value="item.value"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-select
|
||||||
|
v-model="filters.platform"
|
||||||
|
class="console-select"
|
||||||
|
placeholder="全部系统"
|
||||||
|
@change="search"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in PLATFORM_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="filters.range"
|
v-model="filters.range"
|
||||||
class="watch-history-range"
|
class="watch-history-range"
|
||||||
@@ -167,17 +190,16 @@ onMounted(load)
|
|||||||
class="drama-el-table"
|
class="drama-el-table"
|
||||||
empty-text="暂无符合条件的观看记录"
|
empty-text="暂无符合条件的观看记录"
|
||||||
>
|
>
|
||||||
<el-table-column prop="watchedAt" label="观看时间" min-width="170" show-overflow-tooltip />
|
<el-table-column label="观看时间" min-width="170">
|
||||||
<el-table-column label="用户" min-width="180">
|
<template #default="{ row }">{{ formatDateTime(row.watchedAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="用户" min-width="200">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="app-user-meta">
|
<AppUserCell :row="row" />
|
||||||
<span class="app-user-name">{{ row.nickname || '未设置昵称' }}</span>
|
|
||||||
<span class="app-user-uid">{{ row.uid || `用户 #${row.userId || '—'}` }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="系统" width="130" show-overflow-tooltip>
|
<el-table-column label="系统" width="100">
|
||||||
<template #default="{ row }">{{ systemText(row) }}</template>
|
<template #default="{ row }">{{ platformText(row) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="短剧" min-width="190">
|
<el-table-column label="短剧" min-width="190">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
getAppUserApi,
|
getAppUserApi,
|
||||||
@@ -28,6 +29,69 @@ export const COUNTRY_OPTIONS = [
|
|||||||
{ value: 'CN', 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) {
|
export function statusMeta(status) {
|
||||||
return (
|
return (
|
||||||
APP_USER_STATUS.find((item) => item.value === status) || { label: status || '-', tag: 'info' }
|
APP_USER_STATUS.find((item) => item.value === status) || { label: status || '-', tag: 'info' }
|
||||||
@@ -36,12 +100,15 @@ export function statusMeta(status) {
|
|||||||
|
|
||||||
const PLATFORM_TEXT = { ANDROID: 'Android', IOS: 'iOS', WEB: 'Web' }
|
const PLATFORM_TEXT = { ANDROID: 'Android', IOS: 'iOS', WEB: 'Web' }
|
||||||
|
|
||||||
/** 登录会话上的系统,例如 Android 16。没有记录时显示横线。 */
|
/** 客户端平台,只有 iOS / Android 两种取值(缺记录显示横线)。 */
|
||||||
export function systemText(row) {
|
export function platformText(row) {
|
||||||
const name = PLATFORM_TEXT[row?.platform] || row?.platform || ''
|
const platform = row?.platform || ''
|
||||||
const version = row?.osVersion || ''
|
return PLATFORM_TEXT[platform] || platform || '—'
|
||||||
const text = `${name} ${version}`.trim()
|
}
|
||||||
return text || '—'
|
|
||||||
|
/** 后端返回 ISO 时间串,统一格式化成后台可读的本地时间 */
|
||||||
|
export function formatDateTime(value) {
|
||||||
|
return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 毫秒转 mm:ss,观看流水里的进度用 */
|
/** 毫秒转 mm:ss,观看流水里的进度用 */
|
||||||
@@ -60,6 +127,7 @@ export function useAppUsers() {
|
|||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const statusFilter = ref(undefined)
|
const statusFilter = ref(undefined)
|
||||||
const countryFilter = ref(undefined)
|
const countryFilter = ref(undefined)
|
||||||
|
const platformFilter = ref(undefined)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
|
|
||||||
@@ -82,7 +150,8 @@ export function useAppUsers() {
|
|||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
keyword: keyword.value.trim() || undefined,
|
keyword: keyword.value.trim() || undefined,
|
||||||
status: statusFilter.value,
|
status: statusFilter.value,
|
||||||
countryCode: countryFilter.value
|
countryCode: countryFilter.value,
|
||||||
|
platform: platformFilter.value || undefined
|
||||||
})
|
})
|
||||||
users.value = res.data?.list || []
|
users.value = res.data?.list || []
|
||||||
total.value = res.data?.total || 0
|
total.value = res.data?.total || 0
|
||||||
@@ -100,6 +169,7 @@ export function useAppUsers() {
|
|||||||
keyword.value = ''
|
keyword.value = ''
|
||||||
statusFilter.value = undefined
|
statusFilter.value = undefined
|
||||||
countryFilter.value = undefined
|
countryFilter.value = undefined
|
||||||
|
platformFilter.value = undefined
|
||||||
queryUsers()
|
queryUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +256,7 @@ export function useAppUsers() {
|
|||||||
keyword,
|
keyword,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
countryFilter,
|
countryFilter,
|
||||||
|
platformFilter,
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
detailVisible,
|
detailVisible,
|
||||||
|
|||||||
Reference in new issue
Block a user