feat(F006): 前台用户列表(/user/user)对齐企业列表UI标准 - 渐变头像单元(header图/首字)+用户ID两行/手机号图标化/企业认证胶囊+企业名两行/余额货币化/状态圆点胶囊(全局st变量暗色自适应)/批量禁用启用删除/保留tab统计与导出
This commit is contained in:
parent
90d6561746
commit
4c27e6b4b2
@ -1,13 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 用户管理 (前台用户列表) - W17 设计规范 v2 完整版
|
* F006 用户管理 (前台用户列表) - 2026-08-18 按企业列表 UI 标准升级
|
||||||
*
|
*
|
||||||
* 按 ui-preview-v2.html 模板: 9 列 + 6 搜索 + 4 操作 + 状态 tab
|
* 按 ui-preview-v2.html 模板: 列 + 搜索 + 操作 + 状态 tab (W17 功能全部保留)
|
||||||
|
* 后端端点: GET /user/user/page (搜索+分页) / GET /user/user/export (Excel)
|
||||||
|
* POST /user/user/update (禁用/启用) / POST /user/user/delete (删除)
|
||||||
|
* 新增: 用户头像单元(渐变/图片) / 两行信息列 / 状态胶囊(全局 --st-* 变量暗色自适应) / 批量操作栏
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive, computed } from 'vue'
|
import { onMounted, ref, reactive, computed } from 'vue'
|
||||||
import { http, httpGet, httpPost } from '@/utils/request'
|
import { http, httpGet, httpPost } from '@/utils/request'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus, Phone, Clock } from '@element-plus/icons-vue'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
@ -35,6 +38,8 @@ const total = ref(0)
|
|||||||
const enabledStats = reactive({ normal: 0, disabled: 0 })
|
const enabledStats = reactive({ normal: 0, disabled: 0 })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
|
const selectedIds = ref<string[]>([])
|
||||||
|
const selectedCount = ref(0)
|
||||||
|
|
||||||
// 表单弹窗状态
|
// 表单弹窗状态
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
@ -52,49 +57,43 @@ const query = reactive({
|
|||||||
enabled: '' as '' | '0' | '1'
|
enabled: '' as '' | '0' | '1'
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns: any[] = [
|
// 头像品牌渐变色组 (无 header 图片时交替使用, 与企业列表同源风格)
|
||||||
{ type: 'selection', width: 50, fixed: 'left', align: 'center' },
|
const AVATAR_GRADIENTS = [
|
||||||
{ prop: 'id', label: '用户ID', width: 100, align: 'center' },
|
'linear-gradient(135deg, #0b5394, #6fb1e8)',
|
||||||
{ prop: 'nickName', label: '用户昵称', minWidth: 160, align: 'center' },
|
'linear-gradient(135deg, #2563eb, #93c5fd)',
|
||||||
{ prop: 'mobile', label: '手机号', width: 140, align: 'center' },
|
'linear-gradient(135deg, #0e7490, #67e8f9)',
|
||||||
{
|
'linear-gradient(135deg, #7c3aed, #c4b5fd)',
|
||||||
prop: 'isEnterprise',
|
'linear-gradient(135deg, #be123c, #fda4af)'
|
||||||
label: '是否企业认证',
|
|
||||||
width: 140,
|
|
||||||
align: 'center',
|
|
||||||
formatter: (row: FrontUser) => row.isEnterprise === 1 ? '是' : '否'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'enterpriseName',
|
|
||||||
label: '关联企业',
|
|
||||||
minWidth: 200,
|
|
||||||
align: 'center',
|
|
||||||
formatter: (row: FrontUser) => row.enterpriseName || '--'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'accountBalance',
|
|
||||||
label: '平台账户余额',
|
|
||||||
width: 140,
|
|
||||||
align: 'right',
|
|
||||||
formatter: (row: FrontUser) => Number(row.accountBalance || 0).toFixed(2)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'enabled',
|
|
||||||
label: '禁用标识',
|
|
||||||
width: 110,
|
|
||||||
align: 'center',
|
|
||||||
slot: 'enabled'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'createDate',
|
|
||||||
label: '注册时间',
|
|
||||||
width: 180,
|
|
||||||
align: 'center',
|
|
||||||
formatter: (row: FrontUser) => fmtDate(row.createDate)
|
|
||||||
},
|
|
||||||
{ label: '操作', width: 140, slot: 'action', fixed: 'right', align: 'center' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 状态/认证胶囊色 (引用全局 --st-* 变量, 主题切换自动适配暗色)
|
||||||
|
const ENABLED_META: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
|
0: { label: '正常', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
|
1: { label: '禁用', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
|
}
|
||||||
|
const CERT_META: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
|
1: { label: '已认证', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
|
0: { label: '未认证', color: 'var(--st-lv4-fg)', bg: 'var(--st-lv4-bg)' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: any[] = [
|
||||||
|
{ type: 'selection', width: 48, fixed: 'left', align: 'center' },
|
||||||
|
{ label: '用户', minWidth: 200, slot: 'user' },
|
||||||
|
{ prop: 'mobile', label: '手机号', width: 140, slot: 'mobile' },
|
||||||
|
{ label: '企业认证', minWidth: 180, slot: 'enterprise' },
|
||||||
|
{ prop: 'accountBalance', label: '平台余额', width: 140, slot: 'balance', align: 'right' },
|
||||||
|
{ prop: 'enabled', label: '状态', width: 100, slot: 'enabled', align: 'center' },
|
||||||
|
{ label: '注册时间', width: 165, slot: 'createDate' },
|
||||||
|
{ label: '操作', width: 170, slot: 'action', fixed: 'right', align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function avatarStyle(row: FrontUser, index: number) {
|
||||||
|
return { background: AVATAR_GRADIENTS[index % AVATAR_GRADIENTS.length] }
|
||||||
|
}
|
||||||
|
function fmtMoney(n: number): string {
|
||||||
|
const v = Number(n || 0)
|
||||||
|
return `¥ ${v.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||||
|
}
|
||||||
function fmtDate(ts: number | null | undefined) {
|
function fmtDate(ts: number | null | undefined) {
|
||||||
if (!ts) return '--'
|
if (!ts) return '--'
|
||||||
const d = new Date(Number(ts))
|
const d = new Date(Number(ts))
|
||||||
@ -153,9 +152,7 @@ async function loadList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载 stats (normal/disabled 全量计数)
|
* 加载 stats (normal/disabled 全量计数) — 与 tab 无关, 只跟搜索条件关联
|
||||||
* 与 tab 无关, 只跟搜索条件 (userId/nickName/mobile/isEnterprise/enterpriseName) 关联
|
|
||||||
* 通过 limit=1 + page=1 拿到 total (后端只 count 不返列表, 开销可忽略)
|
|
||||||
*/
|
*/
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
@ -177,7 +174,6 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tabItems = computed(() => [
|
const tabItems = computed(() => [
|
||||||
// 全部 = 正常 + 已禁用 (跨 tab 一致, 与搜索条件关联)
|
|
||||||
{ key: 'all', label: '全部', count: enabledStats.normal + enabledStats.disabled },
|
{ key: 'all', label: '全部', count: enabledStats.normal + enabledStats.disabled },
|
||||||
{ key: 'normal', label: '正常', count: enabledStats.normal },
|
{ key: 'normal', label: '正常', count: enabledStats.normal },
|
||||||
{ key: 'disabled', label: '已禁用', count: enabledStats.disabled }
|
{ key: 'disabled', label: '已禁用', count: enabledStats.disabled }
|
||||||
@ -203,6 +199,8 @@ function onReset() {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectionChange(rows: any[]) { selectedIds.value = rows.map((r: any) => r.id); selectedCount.value = rows.length }
|
||||||
|
|
||||||
function onEdit(row: FrontUser) {
|
function onEdit(row: FrontUser) {
|
||||||
editingUserId.value = row.id
|
editingUserId.value = row.id
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@ -219,31 +217,20 @@ function onDialogSuccess() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出当前搜索条件下的用户列表为 Excel
|
* 导出当前搜索条件下的用户列表为 Excel
|
||||||
* 后端: GET /user/user/export (返 Excel 文件流, Content-Type: application/vnd.ms-excel)
|
|
||||||
* 走 http 实例 (自动带 Bearer token), responseType: 'blob' 接二进制流
|
|
||||||
*/
|
*/
|
||||||
async function exportExcel() {
|
async function exportExcel() {
|
||||||
exporting.value = true
|
exporting.value = true
|
||||||
try {
|
try {
|
||||||
const params = buildSearchParams()
|
const params = buildSearchParams()
|
||||||
if (query.enabled !== '') params.enabled = Number(query.enabled)
|
if (query.enabled !== '') params.enabled = Number(query.enabled)
|
||||||
const resp = await http.get<Blob>('/user/user/export', {
|
const resp = await http.get<Blob>('/user/user/export', { params, responseType: 'blob' })
|
||||||
params,
|
|
||||||
responseType: 'blob'
|
|
||||||
})
|
|
||||||
const blob = resp.data
|
const blob = resp.data
|
||||||
if (!blob || blob.size === 0) {
|
if (!blob || blob.size === 0) { ElMessage.error('导出内容为空'); return }
|
||||||
ElMessage.error('导出内容为空')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 优先从响应头取文件名 (后端 ExcelUtils 会设置)
|
|
||||||
const dispo = resp.headers?.['content-disposition'] || resp.headers?.['Content-Disposition']
|
const dispo = resp.headers?.['content-disposition'] || resp.headers?.['Content-Disposition']
|
||||||
let filename = `用户列表_${new Date().toISOString().slice(0, 10)}.xls`
|
let filename = `用户列表_${new Date().toISOString().slice(0, 10)}.xls`
|
||||||
if (typeof dispo === 'string') {
|
if (typeof dispo === 'string') {
|
||||||
const m = dispo.match(/filename\*?=(?:UTF-8\u0027\u0027)?["']?([^;"']+)["']?/i)
|
const m = dispo.match(/filename\*?=(?:UTF-8\u0027\u0027)?["']?([^;"']+)["']?/i)
|
||||||
if (m && m[1]) {
|
if (m && m[1]) { try { filename = decodeURIComponent(m[1]) } catch { filename = m[1] } }
|
||||||
try { filename = decodeURIComponent(m[1]) } catch { filename = m[1] }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const url = URL.createObjectURL(new Blob([blob]))
|
const url = URL.createObjectURL(new Blob([blob]))
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
@ -255,40 +242,57 @@ async function exportExcel() {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
ElMessage.success(`已导出 ${filename}`)
|
ElMessage.success(`已导出 ${filename}`)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const msg = e?.msg || e?.message || '导出失败'
|
ElMessage.error(e?.msg || e?.message || '导出失败')
|
||||||
ElMessage.error(msg)
|
|
||||||
} finally {
|
} finally {
|
||||||
exporting.value = false
|
exporting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 批量操作 (对齐企业列表批量栏) ==========
|
||||||
|
async function batchToggle(next: number) {
|
||||||
|
const action = next === 1 ? '禁用' : '启用'
|
||||||
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择用户'); return }
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认${action}已选 ${selectedIds.value.length} 个用户?`, `批量${action}确认`, { type: 'warning' })
|
||||||
|
} catch { return }
|
||||||
|
let ok = 0
|
||||||
|
for (const id of selectedIds.value) {
|
||||||
|
try { await httpPost('/user/user/update', { id, enabled: next }); ok++ } catch {/* 单条跳过 */}
|
||||||
|
}
|
||||||
|
ElMessage.success(`已${action} ${ok} 个用户`)
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
async function batchDelete() {
|
||||||
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择用户'); return }
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除已选 ${selectedIds.value.length} 个用户?该操作不可恢复`, '批量删除确认', { type: 'warning' })
|
||||||
|
} catch { return }
|
||||||
|
let ok = 0
|
||||||
|
for (const id of selectedIds.value) {
|
||||||
|
try { await httpPost('/user/user/delete', { id }); ok++ } catch {/* 单条跳过 */}
|
||||||
|
}
|
||||||
|
ElMessage.success(`已删除 ${ok} 个用户`)
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleEnabled(row: FrontUser) {
|
async function toggleEnabled(row: FrontUser) {
|
||||||
// 当前 enabled: 0=正常 1=禁用; 要切换到对端
|
|
||||||
const next = row.enabled === 0 ? 1 : 0
|
const next = row.enabled === 0 ? 1 : 0
|
||||||
const action = next === 1 ? '禁用' : '启用'
|
const action = next === 1 ? '禁用' : '启用'
|
||||||
try {
|
try { await ElMessageBox.confirm(`确认${action}用户「${row.nickName}」?`, `${action}确认`, { type: 'warning' }) } catch { return }
|
||||||
await ElMessageBox.confirm(`确认${action}用户「${row.nickName}」?`, `${action}确认`, { type: 'warning' })
|
|
||||||
} catch { return }
|
|
||||||
try {
|
try {
|
||||||
await httpPost('/user/user/update', { id: row.id, enabled: next })
|
await httpPost('/user/user/update', { id: row.id, enabled: next })
|
||||||
ElMessage.success(`${action}成功`)
|
ElMessage.success(`${action}成功`)
|
||||||
row.enabled = next
|
row.enabled = next
|
||||||
} catch (e: any) {
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || `${action}失败`) }
|
||||||
ElMessage.error(e?.msg || e?.message || `${action}失败`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteUser(row: FrontUser) {
|
async function deleteUser(row: FrontUser) {
|
||||||
try {
|
try { await ElMessageBox.confirm(`确认删除用户「${row.nickName}」?该操作不可恢复`, '删除确认', { type: 'warning' }) } catch { return }
|
||||||
await ElMessageBox.confirm(`确认删除用户「${row.nickName}」?该操作不可恢复`, '删除确认', { type: 'warning' })
|
|
||||||
} catch { return }
|
|
||||||
try {
|
try {
|
||||||
await httpPost('/user/user/delete', { id: row.id })
|
await httpPost('/user/user/delete', { id: row.id })
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
load()
|
load()
|
||||||
} catch (e: any) {
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '删除失败') }
|
||||||
ElMessage.error(e?.msg || e?.message || '删除失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
@ -296,18 +300,14 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<PageHeader title="用户列表" :sub="`管理前台注册用户,共 ${enabledStats.normal + enabledStats.disabled} 个账号`">
|
<PageHeader title="用户列表" :sub="`共 ${enabledStats.normal + enabledStats.disabled} 个注册账号`">
|
||||||
<el-button @click="exportExcel" :loading="exporting">导出</el-button>
|
<el-button @click="exportExcel" :loading="exporting">导出</el-button>
|
||||||
<el-button type="primary" @click="onCreate">
|
<el-button type="primary" @click="onCreate">
|
||||||
<el-icon style="margin-right: 4px; vertical-align: -2px;"><Plus /></el-icon>
|
<el-icon style="margin-right: 4px; vertical-align: -2px;"><Plus /></el-icon>
|
||||||
新建用户
|
新建用户
|
||||||
</el-button>
|
</el-button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<UserFormDialog
|
<UserFormDialog v-model:visible="dialogVisible" :user-id="editingUserId" @success="onDialogSuccess" />
|
||||||
v-model:visible="dialogVisible"
|
|
||||||
:user-id="editingUserId"
|
|
||||||
@success="onDialogSuccess"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListTabs v-model="activeTab" :items="tabItems" @change="onTabChange" />
|
<ListTabs v-model="activeTab" :items="tabItems" @change="onTabChange" />
|
||||||
|
|
||||||
@ -325,39 +325,146 @@ onMounted(load)
|
|||||||
@reset="onReset"
|
@reset="onReset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataTable
|
<div class="rr-list-card">
|
||||||
:columns="columns"
|
<!-- 批量操作栏 (对齐企业列表) -->
|
||||||
:data="list"
|
<div class="batch-bar" :class="{ 'has-selection': selectedCount > 0 }">
|
||||||
:loading="loading"
|
<span v-if="selectedCount > 0" class="batch-count">
|
||||||
:total="total"
|
已选 <strong>{{ selectedCount }}</strong> 个用户
|
||||||
:page="query.page"
|
</span>
|
||||||
:limit="query.limit"
|
<span v-else class="batch-hint">勾选用户后可批量操作</span>
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
<span class="batch-actions">
|
||||||
:show-pagination="true"
|
<el-button size="small" type="warning" plain :disabled="!selectedCount" @click="batchToggle(1)">批量禁用</el-button>
|
||||||
:borderless="true"
|
<el-button size="small" type="success" plain :disabled="!selectedCount" @click="batchToggle(0)">批量启用</el-button>
|
||||||
row-key="id"
|
<el-button size="small" type="danger" :disabled="!selectedCount" @click="batchDelete">批量删除</el-button>
|
||||||
empty-text="暂无用户"
|
</span>
|
||||||
@page-change="onPageChange"
|
</div>
|
||||||
@limit-change="onLimitChange"
|
|
||||||
>
|
<DataTable
|
||||||
<template #enabled="{ row }">
|
:columns="columns"
|
||||||
<el-tag v-if="row.enabled === 0" type="success" size="small" effect="light" round>正常</el-tag>
|
:data="list"
|
||||||
<el-tag v-else type="danger" size="small" effect="light" round>禁用</el-tag>
|
:loading="loading"
|
||||||
</template>
|
:total="total"
|
||||||
<template #action="{ row }">
|
:page="query.page"
|
||||||
<TableActions
|
:limit="query.limit"
|
||||||
:row="row"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:actions="[
|
:show-pagination="true"
|
||||||
{ label: '修改', type: 'primary', onClick: onEdit },
|
:borderless="true"
|
||||||
{ label: (r: any) => r.enabled === 0 ? '禁用' : '启用', type: (r: any) => r.enabled === 0 ? 'warning' : 'success', onClick: toggleEnabled },
|
row-key="id"
|
||||||
{ label: '删除', type: 'danger', onClick: deleteUser }
|
empty-text="暂无用户"
|
||||||
]"
|
@page-change="onPageChange"
|
||||||
/>
|
@limit-change="onLimitChange"
|
||||||
</template>
|
@selection-change="onSelectionChange"
|
||||||
</DataTable>
|
>
|
||||||
|
<!-- 用户单元: 头像 + 昵称 + 用户ID -->
|
||||||
|
<template #user="{ row, index }">
|
||||||
|
<div class="user-cell">
|
||||||
|
<span class="user-avatar" :style="avatarStyle(row, index)">
|
||||||
|
<el-image v-if="row.header" :src="row.header" fit="cover" class="user-avatar-img" />
|
||||||
|
<template v-else>{{ (row.nickName || '用').charAt(0) }}</template>
|
||||||
|
</span>
|
||||||
|
<span class="user-text">
|
||||||
|
<span class="user-name">{{ row.nickName }}</span>
|
||||||
|
<span class="user-sub">ID: {{ row.id }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 手机号 -->
|
||||||
|
<template #mobile="{ row }">
|
||||||
|
<span class="phone-cell"><el-icon><Phone /></el-icon>{{ row.mobile }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 企业认证: 胶囊 + 企业名两行 -->
|
||||||
|
<template #enterprise="{ row }">
|
||||||
|
<div class="two-line">
|
||||||
|
<span class="chip" :style="{ color: CERT_META[row.isEnterprise].color, background: CERT_META[row.isEnterprise].bg }">
|
||||||
|
{{ CERT_META[row.isEnterprise].label }}
|
||||||
|
</span>
|
||||||
|
<span class="line-sub">{{ row.enterpriseName || '--' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 平台余额 -->
|
||||||
|
<template #balance="{ row }">
|
||||||
|
<span class="balance-cell">{{ fmtMoney(row.accountBalance) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 状态: 圆点胶囊 -->
|
||||||
|
<template #enabled="{ row }">
|
||||||
|
<span class="status-tag" :style="{ color: ENABLED_META[row.enabled].color, background: ENABLED_META[row.enabled].bg }">
|
||||||
|
<span class="status-dot"></span>{{ ENABLED_META[row.enabled].label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 注册时间 -->
|
||||||
|
<template #createDate="{ row }">
|
||||||
|
<span class="date-cell"><el-icon><Clock /></el-icon>{{ fmtDate(row.createDate) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableActions
|
||||||
|
:row="row"
|
||||||
|
:actions="[
|
||||||
|
{ label: '修改', type: 'primary', onClick: onEdit },
|
||||||
|
{ label: (r: any) => r.enabled === 0 ? '禁用' : '启用', type: (r: any) => r.enabled === 0 ? 'warning' : 'success', onClick: toggleEnabled },
|
||||||
|
{ label: '删除', type: 'danger', onClick: deleteUser }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 100%; }
|
.rr-page { max-width: 100%; padding: 16px; }
|
||||||
|
.rr-list-card {
|
||||||
|
background: var(--surface); border-radius: 12px; overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 批量操作栏 ===== */
|
||||||
|
.batch-bar {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
padding: 12px 16px; border-bottom: 1px solid var(--border);
|
||||||
|
transition: background .2s;
|
||||||
|
}
|
||||||
|
.batch-bar.has-selection { background: var(--primary-soft, #e0ecff); }
|
||||||
|
.batch-count { font-size: 13px; color: var(--ink-2); }
|
||||||
|
.batch-count strong { color: var(--primary); font-size: 15px; }
|
||||||
|
.batch-hint { font-size: 12px; color: var(--ink-3); }
|
||||||
|
.batch-actions { margin-left: auto; display: flex; gap: 4px; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
/* ===== 用户单元 ===== */
|
||||||
|
.user-cell { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.user-avatar {
|
||||||
|
flex: 0 0 38px; width: 38px; height: 38px; border-radius: 10px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
color: #fff; font-size: 16px; font-weight: 700; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.user-avatar-img { width: 100%; height: 100%; border-radius: 10px; }
|
||||||
|
.user-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||||
|
.user-name {
|
||||||
|
font-size: 14px; font-weight: 600; color: var(--ink);
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.user-sub { font-size: 12px; color: var(--ink-3); font-family: 'SF Mono', Menlo, Consolas, monospace; }
|
||||||
|
|
||||||
|
/* ===== 单元格通用 ===== */
|
||||||
|
.two-line { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||||
|
.line-sub { font-size: 12px; color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.phone-cell { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; color: var(--ink); font-family: 'SF Mono', Menlo, Consolas, monospace; }
|
||||||
|
.date-cell { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--ink-2); font-family: 'SF Mono', Menlo, Consolas, monospace; }
|
||||||
|
.balance-cell { font-size: 13px; font-weight: 600; color: var(--ink); font-family: 'SF Mono', Menlo, Consolas, monospace; }
|
||||||
|
|
||||||
|
/* ===== 胶囊 (对齐企业列表, 全局 --st-* 变量暗色自适应) ===== */
|
||||||
|
.chip {
|
||||||
|
display: inline-flex; align-items: center; width: fit-content;
|
||||||
|
font-size: 12px; font-weight: 500; padding: 2px 12px; border-radius: 20px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.status-tag {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
font-size: 12px; font-weight: 500; padding: 3px 12px; border-radius: 20px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||||
</style>
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user