fix(F006-W17.3.3): SearchBar select width 显式 px + 列宽 isEnterprise 140 + min-width 兜底

This commit is contained in:
Sisyphus 2026-07-07 15:42:59 +08:00
parent 63420203dd
commit 0753ef271f
4 changed files with 849 additions and 25 deletions

251
src/InquiryListView.vue Normal file
View 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
View 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
View 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>

View File

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