fix(F006-W17.3.3): SearchBar select width 显式 px + 列宽 isEnterprise 140 + min-width 兜底
This commit is contained in:
parent
63420203dd
commit
0753ef271f
251
src/InquiryListView.vue
Normal file
251
src/InquiryListView.vue
Normal file
@ -0,0 +1,251 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 询价管理列表 - W4.9 套 v1.6 设计规范
|
||||
*
|
||||
* 后端: GET /sys/openapi/inquiry/list?page=&limit=&status=
|
||||
* 状态: pending(0) 待报价 / quoting(1) 报价中 / done(2) 已成交 / cancelled(3) 已取消
|
||||
*
|
||||
* 设计规范:§16 列表页完整模板 + §17.7 紧凑弹窗 + §17.8 主题色弹窗 + §17.9 列优化
|
||||
*/
|
||||
import { onMounted, computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||
import { useInquiryStore, STATUS_OPTIONS } from '@/stores/inquiry'
|
||||
import { httpGet } from '@/utils/request'
|
||||
|
||||
interface InquiryRow {
|
||||
id: string
|
||||
customer: string
|
||||
product: string
|
||||
qty: string
|
||||
status: 'pending' | 'quoting' | 'done' | 'cancelled' | string
|
||||
time: string | number
|
||||
raw: any
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const store = useInquiryStore()
|
||||
|
||||
const STATUS_TABS = STATUS_OPTIONS.map(o => ({ value: o.value, label: o.label }))
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
pending: '待报价',
|
||||
quoting: '报价中',
|
||||
done: '已成交',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
|
||||
const list = ref<InquiryRow[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(10)
|
||||
|
||||
const filters = ref({
|
||||
status: '',
|
||||
customer: '',
|
||||
searchKey: ''
|
||||
})
|
||||
|
||||
function fmtDate(ts?: string | number | null) {
|
||||
if (!ts) return '--'
|
||||
let n: number
|
||||
if (typeof ts === 'string') {
|
||||
n = /^\d+$/.test(ts) ? Number(ts) : Date.parse(ts)
|
||||
} else {
|
||||
n = ts
|
||||
}
|
||||
if (!n || isNaN(n)) return String(ts)
|
||||
const d = new Date(n)
|
||||
if (isNaN(d.getTime())) return String(ts)
|
||||
const pad = (x: number) => String(x).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await httpGet<any>('/sys/openapi/inquiry/list', {
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
...(filters.value.status ? { status: filters.value.status } : {})
|
||||
})
|
||||
const arr = Array.isArray(data) ? data : []
|
||||
list.value = arr.map((it: any) => ({
|
||||
id: String(it.id ?? ''),
|
||||
customer: it.inquiryEnterpriseName || it.customer || it.customerName || '--',
|
||||
product: it.title || it.product || it.productName || '--',
|
||||
qty: it.purchaseQuantity || it.qty || '--',
|
||||
status: it.status || (it.apprStatus != null ? (['pending','quoting','done','cancelled'][it.apprStatus] || 'pending') : 'pending'),
|
||||
time: it.publishDate || it.createDate || it.time,
|
||||
raw: it
|
||||
}))
|
||||
total.value = arr.length + (page.value - 1) * limit.value
|
||||
} catch (e: any) {
|
||||
error.value = e?.msg || e?.message || '后端接口异常(详见 console)'
|
||||
list.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.value.status = ''; filters.value.customer = ''; filters.value.searchKey = ''; page.value = 1; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||
function onStatusTab(v: string) {
|
||||
filters.value.status = v
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
function onRowClick(row: InquiryRow) { router.push(`/inquiry/${row.id}`) }
|
||||
function onView(row: InquiryRow) { router.push(`/inquiry/${row.id}`) }
|
||||
|
||||
const columns = computed(() => [
|
||||
{ prop: 'id', label: '单号', width: 180 },
|
||||
{ prop: 'customer', label: '客户', minWidth: 180 },
|
||||
{ prop: 'product', label: '商品', minWidth: 180 },
|
||||
{ prop: 'qty', label: '数量', width: 120 },
|
||||
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
||||
{ prop: 'time', label: '时间', width: 170, slot: 'time' },
|
||||
{ label: '操作', width: 100, slot: 'action' }
|
||||
])
|
||||
|
||||
onMounted(() => { load() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-inquiry-list">
|
||||
<PageHeader title="询价管理" :sub="`共 ${total} 条询价`">
|
||||
<el-button @click="onReset">重置</el-button>
|
||||
</PageHeader>
|
||||
|
||||
<div class="rr-tabs-row">
|
||||
<div class="rr-tabs">
|
||||
<div
|
||||
v-for="t in STATUS_TABS"
|
||||
:key="t.value"
|
||||
class="rr-tab"
|
||||
:class="{ active: filters.status === t.value }"
|
||||
@click="onStatusTab(t.value)"
|
||||
>{{ t.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchBar
|
||||
v-model:filters="filters"
|
||||
:fields='[{ key: "searchKey", label: "", type: "text", placeholder: "搜索单号/客户/商品", clearable: true }]'
|
||||
:show-reset="false"
|
||||
@search="onSearch"
|
||||
/>
|
||||
|
||||
<div class="rr-list-card">
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="true"
|
||||
:error="error"
|
||||
empty-text="暂无询价"
|
||||
borderless
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
@row-click="onRowClick"
|
||||
>
|
||||
<template #status="{ row }">
|
||||
<span :class="['rr-badge', `rr-badge-${row.status}`]">{{ STATUS_LABEL[row.status] || row.status }}</span>
|
||||
</template>
|
||||
<template #time="{ row }">
|
||||
<span class="rr-time">{{ fmtDate(row.time) }}</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<el-button text type="primary" size="small" @click.stop="onView(row)">查看</el-button>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rr-inquiry-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.rr-tabs-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.rr-tabs {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rr-tab {
|
||||
position: relative;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
.rr-tab:hover { color: var(--el-text-color-primary); }
|
||||
.rr-tab.active {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.rr-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.rr-list-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.rr-time {
|
||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
/* 状态徽章(4 种) - 浅底色块(套设计规范 §5.4) */
|
||||
.rr-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 500;
|
||||
line-height: 17.25px;
|
||||
height: 21.25px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.rr-badge-pending { background: #FEF3C7; color: #D97706; }
|
||||
.rr-badge-quoting { background: #DBEAFE; color: #2563EB; }
|
||||
.rr-badge-done { background: #D1FAE5; color: #059669; }
|
||||
.rr-badge-cancelled { background: #FEE2E2; color: #DC2626; }
|
||||
</style>
|
||||
274
src/SearchBar.vue
Normal file
274
src/SearchBar.vue
Normal file
@ -0,0 +1,274 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 通用 SearchBar 组件 (W17 设计规范 v2 整改)
|
||||
*
|
||||
* 按 ui-preview-v2.html 模板: 紧凑布局 + 无 label + 同行末尾 chip + 简洁按钮
|
||||
*
|
||||
* 用法:
|
||||
* <SearchBar
|
||||
* v-model:filters="filters"
|
||||
* :fields="[
|
||||
* { key: 'name', label: '名称', type: 'text', placeholder: '请输入' },
|
||||
* { key: 'status', label: '状态', type: 'select', options: [...] }
|
||||
* ]"
|
||||
* @search="onSearch"
|
||||
* @reset="onReset"
|
||||
* />
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
|
||||
export interface SearchFieldOption {
|
||||
value: any
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface SearchField {
|
||||
key: string
|
||||
label: string
|
||||
type?: 'text' | 'select' | 'date' | 'daterange' | 'number'
|
||||
placeholder?: string
|
||||
options?: SearchFieldOption[]
|
||||
width?: number | string
|
||||
clearable?: boolean
|
||||
showLabel?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
filters: Record<string, any>
|
||||
fields: SearchField[]
|
||||
showReset?: boolean
|
||||
showChips?: boolean
|
||||
showLabel?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showReset: true,
|
||||
showChips: true,
|
||||
showLabel: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:filters', filters: Record<string, any>): void
|
||||
(e: 'search'): void
|
||||
(e: 'reset'): void
|
||||
}>()
|
||||
|
||||
const localFilters = computed({
|
||||
get: () => props.filters,
|
||||
set: (v) => emit('update:filters', v)
|
||||
})
|
||||
|
||||
function onSearch() { emit('search') }
|
||||
function onReset() {
|
||||
for (const f of props.fields) {
|
||||
if (f.type === 'daterange') localFilters.value[f.key] = []
|
||||
else if (f.type === 'number') localFilters.value[f.key] = undefined
|
||||
else localFilters.value[f.key] = ''
|
||||
}
|
||||
emit('reset')
|
||||
}
|
||||
function onEnter() { onSearch() }
|
||||
function onClearField(key: string) {
|
||||
const f = props.fields.find(it => it.key === key)
|
||||
if (!f) return
|
||||
if (f.type === 'daterange') localFilters.value[key] = []
|
||||
else if (f.type === 'number') localFilters.value[key] = undefined
|
||||
else localFilters.value[key] = ''
|
||||
}
|
||||
|
||||
const activeChips = computed(() => {
|
||||
return props.fields
|
||||
.map(f => {
|
||||
const v = props.filters[f.key]
|
||||
let display: string | null = null
|
||||
if (f.type === 'daterange') {
|
||||
if (Array.isArray(v) && v.length === 2) display = `${v[0]} ~ ${v[1]}`
|
||||
} else if (f.type === 'select' && v !== '' && v !== undefined && v !== null) {
|
||||
const opt = f.options?.find(o => String(o.value) === String(v))
|
||||
display = opt ? opt.label : String(v)
|
||||
} else if (v !== '' && v !== undefined && v !== null) {
|
||||
display = String(v)
|
||||
}
|
||||
if (display) {
|
||||
return { key: f.key, label: `${f.label}: ${display}` }
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean) as { key: string; label: string }[]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-search-bar">
|
||||
<div class="rr-search-bar__row">
|
||||
<div class="rr-search-bar__field" v-for="f in fields" :key="f.key">
|
||||
<span v-if="showLabel || f.showLabel" class="rr-search-bar__label">{{ f.label }}</span>
|
||||
<el-input
|
||||
v-if="f.type === 'text' || !f.type"
|
||||
v-model="localFilters[f.key]"
|
||||
:placeholder="f.placeholder || `请输入${f.label}`"
|
||||
:clearable="f.clearable !== false"
|
||||
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
|
||||
@keyup.enter="onEnter"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="f.type === 'select'"
|
||||
v-model="localFilters[f.key]"
|
||||
:placeholder="f.placeholder || `请选择${f.label}`"
|
||||
:clearable="f.clearable !== false"
|
||||
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '160px')) }"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in f.options || []"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-else-if="f.type === 'date'"
|
||||
v-model="localFilters[f.key]"
|
||||
type="date"
|
||||
:placeholder="f.placeholder || `请选择${f.label}`"
|
||||
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-else-if="f.type === 'daterange'"
|
||||
v-model="localFilters[f.key]"
|
||||
type="daterange"
|
||||
range-separator="~"
|
||||
:start-placeholder="f.placeholder || '开始日期'"
|
||||
:end-placeholder="'结束日期'"
|
||||
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '260px')) }"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="f.type === 'number'"
|
||||
v-model="localFilters[f.key]"
|
||||
:placeholder="f.placeholder || `请输入${f.label}`"
|
||||
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
|
||||
/>
|
||||
</div>
|
||||
<div class="rr-search-bar__chips" v-if="showChips && activeChips.length">
|
||||
<div
|
||||
v-for="c in activeChips"
|
||||
:key="c.key"
|
||||
class="rr-search-bar__chip"
|
||||
>
|
||||
{{ c.label }}
|
||||
<span class="rr-search-bar__chip-close" @click="onClearField(c.key)">×</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rr-search-bar__actions">
|
||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||
<el-button v-if="showReset" @click="onReset">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* === 统一 input/select 圆角 (按 ui-preview-v2.html 模板) === */
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-select__wrapper) {
|
||||
border-radius: var(--radius, 8px);
|
||||
box-shadow: 0 0 0 1px var(--rr-border-color, #E2E8F0) inset;
|
||||
transition: box-shadow 0.15s, border-color 0.15s;
|
||||
min-height: 34px;
|
||||
}
|
||||
:deep(.el-input__wrapper:hover),
|
||||
:deep(.el-select__wrapper:hover) {
|
||||
box-shadow: 0 0 0 1px var(--ink-3, #94A3B8) inset;
|
||||
}
|
||||
:deep(.el-input__wrapper.is-focus),
|
||||
:deep(.el-select__wrapper.is-focused) {
|
||||
box-shadow: 0 0 0 1px var(--brand, #2563EB) inset, 0 0 0 3px var(--brand-glow, rgba(37, 99, 235, 0.18));
|
||||
}
|
||||
/* === el-select 宽度兜底 (Vue :style 没生效时也保证最小宽度) === */
|
||||
:deep(.el-select) {
|
||||
min-width: 140px;
|
||||
width: auto !important;
|
||||
}
|
||||
:deep(.el-select .el-select__wrapper) {
|
||||
min-width: 140px;
|
||||
}
|
||||
:deep(.el-select .el-select__wrapper .el-select__selection) {
|
||||
min-width: 100px;
|
||||
}
|
||||
/* === 下拉弹层圆角 (跟 input 一致) === */
|
||||
:deep(.el-select-dropdown),
|
||||
:deep(.el-select__popper),
|
||||
:deep(.el-popper.is-light) {
|
||||
border-radius: var(--radius, 8px) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.el-select-dropdown__item) {
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
margin: 2px 4px;
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
}
|
||||
:deep(.el-select-dropdown__item.is-hovering) {
|
||||
background: var(--brand-soft, rgba(37, 99, 235, 0.08));
|
||||
color: var(--brand, #2563EB);
|
||||
}
|
||||
:deep(.el-select-dropdown__item.is-selected) {
|
||||
font-weight: 600;
|
||||
color: var(--brand, #2563EB);
|
||||
}
|
||||
|
||||
.rr-search-bar {
|
||||
margin-bottom: 16px;
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
&__field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
&__label {
|
||||
font-size: 13px;
|
||||
color: var(--ink-2, #334155);
|
||||
margin-right: 6px;
|
||||
}
|
||||
&__chips {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
&__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
background: var(--brand-soft, rgba(37, 99, 235, 0.08));
|
||||
color: var(--brand, #2563EB);
|
||||
border-radius: var(--radius-pill, 9999px);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
&-close {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
:deep(.el-button) {
|
||||
padding: 7px 16px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius, 8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
270
src/UserListView.vue
Normal file
270
src/UserListView.vue
Normal file
@ -0,0 +1,270 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 用户管理 (前台用户列表) - W17 设计规范 v2 完整版
|
||||
*
|
||||
* 按 ui-preview-v2.html 模板: 9 列 + 6 搜索 + 4 操作 + 状态 tab
|
||||
*/
|
||||
import { onMounted, ref, reactive, computed } from 'vue'
|
||||
import { httpGet, httpPost } from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||||
|
||||
interface FrontUser {
|
||||
id: string
|
||||
nickName: string
|
||||
mobile: string
|
||||
header?: string | null
|
||||
isEnterprise: number
|
||||
enterpriseName?: string | null
|
||||
enterpriseId?: string | null
|
||||
accountBalance: number
|
||||
enabled: number
|
||||
createDate: number
|
||||
isDel: number
|
||||
raw: any
|
||||
}
|
||||
|
||||
const list = ref<FrontUser[]>([])
|
||||
const total = ref(0)
|
||||
const enabledStats = reactive({ normal: 0, disabled: 0 })
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('all')
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
userId: '',
|
||||
nickName: '',
|
||||
mobile: '',
|
||||
enterpriseName: '',
|
||||
isEnterprise: '' as '' | '0' | '1',
|
||||
enabled: '' as '' | '0' | '1'
|
||||
})
|
||||
|
||||
const columns: any[] = [
|
||||
{ type: 'selection', width: 50, fixed: 'left' },
|
||||
{ prop: 'id', label: '用户ID', width: 100 },
|
||||
{ prop: 'nickName', label: '用户昵称', minWidth: 160 },
|
||||
{ prop: 'mobile', label: '手机号', width: 140 },
|
||||
{
|
||||
prop: 'isEnterprise',
|
||||
label: '是否企业认证',
|
||||
width: 140,
|
||||
formatter: (row: FrontUser) => row.isEnterprise === 1 ? '是' : '否'
|
||||
},
|
||||
{
|
||||
prop: 'enterpriseName',
|
||||
label: '关联企业',
|
||||
minWidth: 200,
|
||||
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,
|
||||
slot: 'enabled'
|
||||
},
|
||||
{
|
||||
prop: 'createDate',
|
||||
label: '注册时间',
|
||||
width: 180,
|
||||
formatter: (row: FrontUser) => fmtDate(row.createDate)
|
||||
},
|
||||
{ label: '操作', width: 220, slot: 'action', fixed: 'right' }
|
||||
]
|
||||
|
||||
function fmtDate(ts: number | null | undefined) {
|
||||
if (!ts) return '--'
|
||||
const d = new Date(Number(ts))
|
||||
if (isNaN(d.getTime())) return '--'
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = { page: query.page, limit: query.limit }
|
||||
if (query.userId) params.id = query.userId
|
||||
if (query.nickName) params.nickName = query.nickName
|
||||
if (query.mobile) params.mobile = query.mobile
|
||||
if (query.enterpriseName) params.enterpriseName = query.enterpriseName
|
||||
if (query.isEnterprise !== '') params.isEnterprise = Number(query.isEnterprise)
|
||||
if (query.enabled !== '') params.enabled = Number(query.enabled)
|
||||
const resp: any = await httpGet('/user/user/page', params)
|
||||
if (resp && Array.isArray(resp.list)) {
|
||||
list.value = resp.list.map((it: any) => ({
|
||||
id: it.id,
|
||||
nickName: it.nickName || '--',
|
||||
mobile: it.mobile || '--',
|
||||
header: it.header || null,
|
||||
isEnterprise: Number(it.isEnterprise ?? 0),
|
||||
enterpriseName: it.enterpriseName || null,
|
||||
enterpriseId: it.enterpriseId || null,
|
||||
accountBalance: Number(it.accountBalance ?? 0),
|
||||
enabled: Number(it.enabled ?? 0),
|
||||
createDate: Number(it.createDate ?? 0),
|
||||
isDel: Number(it.isDel ?? 0),
|
||||
raw: it
|
||||
}))
|
||||
total.value = Number(resp.total || 0)
|
||||
// 统计 (在当前搜索结果中)
|
||||
enabledStats.normal = list.value.filter(u => u.enabled === 1).length
|
||||
enabledStats.disabled = list.value.filter(u => u.enabled === 0).length
|
||||
} else {
|
||||
list.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (e: any) {
|
||||
list.value = []
|
||||
total.value = 0
|
||||
ElMessage.error(e?.msg || e?.message || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const tabItems = computed(() => [
|
||||
{ key: 'all', label: '全部', count: total.value },
|
||||
{ key: 'normal', label: '正常', count: enabledStats.normal },
|
||||
{ key: 'disabled', label: '已禁用', count: enabledStats.disabled }
|
||||
])
|
||||
|
||||
function onTabChange(key: string) {
|
||||
if (key === 'all') query.enabled = ''
|
||||
else if (key === 'normal') query.enabled = '1'
|
||||
else if (key === 'disabled') query.enabled = '0'
|
||||
query.page = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function onPageChange(p: number) { query.page = p; load() }
|
||||
function onSearch() { query.page = 1; load() }
|
||||
function onReset() {
|
||||
query.userId = ''; query.nickName = ''; query.mobile = ''
|
||||
query.enterpriseName = ''; query.isEnterprise = ''; query.enabled = ''
|
||||
activeTab.value = 'all'
|
||||
query.page = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function onEdit(row: FrontUser) {
|
||||
ElMessageBox.alert(`用户详情编辑待 W17 完善 (当前用户: ${row.nickName}, ID: ${row.id})`, '修改用户', { type: 'info' })
|
||||
}
|
||||
|
||||
async function toggleEnabled(row: FrontUser) {
|
||||
const next = row.enabled === 1 ? 0 : 1
|
||||
const action = next === 1 ? '启用' : '禁用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${action}用户「${row.nickName}」?`, `${action}确认`, { type: 'warning' })
|
||||
} catch { return }
|
||||
try {
|
||||
await httpPost('/user/user/update', { id: row.id, enabled: next })
|
||||
ElMessage.success(`${action}成功`)
|
||||
row.enabled = next
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || `${action}失败`)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(row: FrontUser) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除用户「${row.nickName}」?该操作不可恢复`, '删除确认', { type: 'warning' })
|
||||
} catch { return }
|
||||
try {
|
||||
await httpPost('/user/user/delete', { id: row.id })
|
||||
ElMessage.success('删除成功')
|
||||
load()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<PageHeader title="用户列表" :sub="`管理前台注册用户,共 ${total} 个账号`">
|
||||
<el-button @click="ElMessageBox.alert('导出功能待 W17 完善', '提示', { type: 'info' })">导出</el-button>
|
||||
<el-button type="primary" @click="ElMessageBox.alert('新增用户功能待 W17 完善', '提示', { type: 'info' })">
|
||||
<el-icon style="margin-right: 4px; vertical-align: -2px;"><Plus /></el-icon>
|
||||
新建用户
|
||||
</el-button>
|
||||
</PageHeader>
|
||||
|
||||
<ListTabs v-model="activeTab" :items="tabItems" @change="onTabChange" />
|
||||
|
||||
<SearchBar
|
||||
v-model:filters="query"
|
||||
:fields="[
|
||||
{ key: 'userId', label: '用户ID', type: 'text', placeholder: '用户ID' },
|
||||
{ key: 'nickName', label: '用户昵称', type: 'text', placeholder: '用户昵称' },
|
||||
{ key: 'mobile', label: '手机号', type: 'text', placeholder: '手机号' },
|
||||
{ key: 'isEnterprise', label: '是否企业认证', type: 'select', placeholder: '所有状态', width: '160px',
|
||||
options: [{ label: '已认证', value: '1' }, { label: '未认证', value: '0' }] },
|
||||
{ key: 'enterpriseName', label: '企业名称', type: 'text', placeholder: '企业名称' }
|
||||
]"
|
||||
@search="onSearch"
|
||||
@reset="onReset"
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="query.page"
|
||||
:limit="query.limit"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:show-pagination="true"
|
||||
:borderless="true"
|
||||
row-key="id"
|
||||
empty-text="暂无用户"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #enabled="{ row }">
|
||||
<el-tag v-if="row.enabled === 1" type="success" size="small" effect="light" round>正常</el-tag>
|
||||
<el-tag v-else type="danger" size="small" effect="light" round>禁用</el-tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<span class="rr-actions">
|
||||
<el-link type="primary" :underline="false" @click="onEdit(row)">修改</el-link>
|
||||
<span class="rr-actions__sep">|</span>
|
||||
<el-link :type="row.enabled === 1 ? 'warning' : 'success'" :underline="false" @click="toggleEnabled(row)">
|
||||
{{ row.enabled === 1 ? '禁用' : '启用' }}
|
||||
</el-link>
|
||||
<span class="rr-actions__sep">|</span>
|
||||
<el-link type="danger" :underline="false" @click="deleteUser(row)">删除</el-link>
|
||||
</span>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.rr-page { max-width: 100%; }
|
||||
.rr-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
&__sep {
|
||||
color: var(--rr-border-color, #E2E8F0);
|
||||
margin: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(.el-link) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,29 +1,53 @@
|
||||
/* === 全局弹层圆角 (按 ui-preview-v2.html 模板: --radius 8px) ===
|
||||
EP 弹层 (select-dropdown / popper / dropdown / menu) 通过 teleport 到 body,
|
||||
不在组件 scope 内, 必须全局生效 */
|
||||
不在组件 scope 内, 必须全局生效。双重保险:① 改 EP CSS 变量根 ② 写 !important selector */
|
||||
|
||||
/* === 1) 改 EP CSS 变量根 (从源头覆盖, 所有用 var(--el-*) 的弹层都生效) === */
|
||||
:root {
|
||||
--el-border-radius-base: 8px;
|
||||
--el-border-radius-small: 6px;
|
||||
--el-popper-border-radius: 8px;
|
||||
}
|
||||
|
||||
/* === 2) 显式 selector + !important (覆盖任何内联样式) === */
|
||||
.el-select-dropdown,
|
||||
.el-select__popper,
|
||||
.el-select-v2__popper,
|
||||
.el-dropdown-menu,
|
||||
.el-dropdown__popper,
|
||||
.el-menu--popup,
|
||||
.el-popper,
|
||||
.el-popper.is-light,
|
||||
.el-picker__popper,
|
||||
.el-autocomplete__popper,
|
||||
.el-autocomplete-suggestion,
|
||||
.el-popper {
|
||||
.el-cascader__dropdown,
|
||||
.el-cascader-panel,
|
||||
.el-color-picker__panel,
|
||||
.el-tooltip__popper,
|
||||
.el-message-box,
|
||||
.el-notification,
|
||||
.el-drawer {
|
||||
border-radius: var(--radius, 8px) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* dropdown 项 hover/选中态 */
|
||||
/* 弹层内容滚动层 (Element Plus 弹层有 .el-popper__content 内部容器) */
|
||||
.el-popper .el-popper__content,
|
||||
.el-select-dropdown .el-scrollbar {
|
||||
border-radius: var(--radius, 8px) !important;
|
||||
}
|
||||
|
||||
/* === 3) dropdown 项 hover/选中态 (跟模版统一: brand-soft 高亮 + brand 蓝字) === */
|
||||
.el-select-dropdown__item,
|
||||
.el-dropdown-menu__item {
|
||||
border-radius: var(--radius-sm, 6px) !important;
|
||||
margin: 2px 4px;
|
||||
padding: 0 12px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
font-size: 13px;
|
||||
margin: 2px 4px !important;
|
||||
padding: 0 12px !important;
|
||||
height: 34px !important;
|
||||
line-height: 34px !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--ink-2, #334155);
|
||||
background: transparent;
|
||||
}
|
||||
.el-select-dropdown__item.is-hovering,
|
||||
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||
@ -31,30 +55,35 @@
|
||||
color: var(--brand, #2563EB) !important;
|
||||
}
|
||||
.el-select-dropdown__item.is-selected {
|
||||
font-weight: 600;
|
||||
font-weight: 600 !important;
|
||||
color: var(--brand, #2563EB) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
/* 弹层背景跟主题 (避免 dark 下弹层仍是浅色 + 文字看不清) */
|
||||
/* === 4) 弹层背景跟主题 (避免 dark 下弹层仍是浅色 + 文字看不清) === */
|
||||
.el-select-dropdown,
|
||||
.el-dropdown-menu,
|
||||
.el-popper.is-light {
|
||||
.el-popper.is-light,
|
||||
.el-cascader__dropdown,
|
||||
.el-color-picker__panel {
|
||||
background: var(--surface, #FFFFFF) !important;
|
||||
border: 1px solid var(--border, #E2E8F0) !important;
|
||||
}
|
||||
.el-select-dropdown__item,
|
||||
.el-dropdown-menu__item {
|
||||
color: var(--ink-2, #334155);
|
||||
background: transparent;
|
||||
|
||||
/* === 5) input 圆角: 跟模版一致 (按 ui-preview-v2.html: 1px 描边 + 8px 圆角 + 34px 高) === */
|
||||
.el-input__wrapper,
|
||||
.el-select__wrapper,
|
||||
.el-textarea__inner {
|
||||
border-radius: var(--radius, 8px) !important;
|
||||
box-shadow: 0 0 0 1px var(--border, #E2E8F0) inset !important;
|
||||
min-height: 34px;
|
||||
transition: box-shadow 0.15s, border-color 0.15s;
|
||||
}
|
||||
.el-select-dropdown__item.is-hovering,
|
||||
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||
background: var(--brand-soft) !important;
|
||||
color: var(--brand) !important;
|
||||
.el-input__wrapper:hover,
|
||||
.el-select__wrapper:hover {
|
||||
box-shadow: 0 0 0 1px var(--ink-3, #94A3B8) inset !important;
|
||||
}
|
||||
.el-select-dropdown__item.is-selected {
|
||||
color: var(--brand) !important;
|
||||
font-weight: 600;
|
||||
background: transparent !important;
|
||||
.el-input__wrapper.is-focus,
|
||||
.el-select__wrapper.is-focused {
|
||||
box-shadow: 0 0 0 1px var(--brand, #2563EB) inset, 0 0 0 3px var(--brand-glow, rgba(37, 99, 235, 0.18)) !important;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user