admin_new P0+P1 全批次合并 (84 commits) #2
@ -356,7 +356,7 @@ onMounted(loadBadges)
|
|||||||
.rr-layout {
|
.rr-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: var(--bg-soft);
|
background: var(--rr-page-bg, #F1F5F9);
|
||||||
transition: background 0.3s;
|
transition: background 0.3s;
|
||||||
&.collapsed .rr-sidebar { width: 64px; }
|
&.collapsed .rr-sidebar { width: 64px; }
|
||||||
}
|
}
|
||||||
@ -457,7 +457,7 @@ onMounted(loadBadges)
|
|||||||
transition: all 0.12s;
|
transition: all 0.12s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover { background: var(--bg-soft); color: var(--ink); }
|
&:hover { background: var(--rr-page-bg, #F1F5F9); color: var(--ink); }
|
||||||
&.active {
|
&.active {
|
||||||
background: var(--brand-soft);
|
background: var(--brand-soft);
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
@ -475,7 +475,7 @@ onMounted(loadBadges)
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 1px 7px;
|
padding: 1px 7px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: var(--bg-soft);
|
background: var(--rr-page-bg, #F1F5F9);
|
||||||
color: var(--ink-3);
|
color: var(--ink-3);
|
||||||
|
|
||||||
.rr-sidebar-item.active & { background: rgba(255,255,255,0.18); color: var(--brand); }
|
.rr-sidebar-item.active & { background: rgba(255,255,255,0.18); color: var(--brand); }
|
||||||
@ -501,7 +501,7 @@ onMounted(loadBadges)
|
|||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover { background: var(--bg-soft); color: var(--ink); }
|
&:hover { background: var(--rr-page-bg, #F1F5F9); color: var(--ink); }
|
||||||
&.active {
|
&.active {
|
||||||
background: var(--brand-soft);
|
background: var(--brand-soft);
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
@ -602,7 +602,7 @@ onMounted(loadBadges)
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--ink-2);
|
color: var(--ink-2);
|
||||||
&:hover { background: var(--bg-soft); color: var(--brand); }
|
&:hover { background: var(--rr-page-bg, #F1F5F9); color: var(--brand); }
|
||||||
}
|
}
|
||||||
.rr-topbar-breadcrumb { font-size: 13px; }
|
.rr-topbar-breadcrumb { font-size: 13px; }
|
||||||
|
|
||||||
@ -627,7 +627,7 @@ onMounted(loadBadges)
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover { background: var(--bg-soft); }
|
&:hover { background: var(--rr-page-bg, #F1F5F9); }
|
||||||
}
|
}
|
||||||
.rr-topbar-avatar {
|
.rr-topbar-avatar {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
@ -652,7 +652,7 @@ onMounted(loadBadges)
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 24px 28px;
|
padding: 24px 28px;
|
||||||
background: var(--bg-soft);
|
background: var(--rr-page-bg, #F1F5F9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== W5.x v2 Design Language 新增样式 ===== */
|
/* ===== W5.x v2 Design Language 新增样式 ===== */
|
||||||
@ -662,7 +662,7 @@ onMounted(loadBadges)
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--bg-soft);
|
background: var(--rr-page-bg, #F1F5F9);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -691,7 +691,7 @@ img.rr-sidebar-logo {
|
|||||||
transition: all 0.2s var(--ease);
|
transition: all 0.2s var(--ease);
|
||||||
}
|
}
|
||||||
.rr-topbar-notification:hover {
|
.rr-topbar-notification:hover {
|
||||||
background: var(--bg-soft);
|
background: var(--rr-page-bg, #F1F5F9);
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
}
|
}
|
||||||
.rr-topbar-notification-icon {
|
.rr-topbar-notification-icon {
|
||||||
@ -733,7 +733,7 @@ img.rr-sidebar-logo {
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--bg-soft);
|
background: var(--rr-page-bg, #F1F5F9);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -762,7 +762,7 @@ img.rr-sidebar-logo {
|
|||||||
transition: all 0.2s var(--ease);
|
transition: all 0.2s var(--ease);
|
||||||
}
|
}
|
||||||
.rr-topbar-notification:hover {
|
.rr-topbar-notification:hover {
|
||||||
background: var(--bg-soft);
|
background: var(--rr-page-bg, #F1F5F9);
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
}
|
}
|
||||||
.rr-topbar-notification-icon {
|
.rr-topbar-notification-icon {
|
||||||
|
|||||||
@ -59,6 +59,7 @@ interface Props {
|
|||||||
page?: number
|
page?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
showPagination?: boolean
|
showPagination?: boolean
|
||||||
|
carded?: boolean
|
||||||
pageSizes?: number[]
|
pageSizes?: number[]
|
||||||
rowKey?: string
|
rowKey?: string
|
||||||
stripe?: boolean
|
stripe?: boolean
|
||||||
@ -111,36 +112,36 @@ function onLimitChange(l: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-data-table" :class="{ borderless }">
|
<div class="rr-data-table" :class="{ borderless, carded }">
|
||||||
<el-table
|
<el-table
|
||||||
:data="data"
|
:data="data"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:stripe="stripe"
|
:stripe="stripe"
|
||||||
:border="!borderless"
|
:border="!borderless"
|
||||||
:row-key="rowKey"
|
:row-key="rowKey"
|
||||||
style="width: 100%"
|
style="width: 100%" :cell-class-name="'rr-data-cell'"
|
||||||
@row-click="onRowClick"
|
@row-click="onRowClick"
|
||||||
@selection-change="onSelectionChange"
|
@selection-change="onSelectionChange"
|
||||||
row-class-name="rr-clickable-row"
|
row-class-name="rr-clickable-row"
|
||||||
:header-row-style="{ height: '40px' }"
|
:header-row-style="{ height: '40px' }"
|
||||||
:row-style="{ height: '52px' }"
|
:row-style="{ height: '52px' }"
|
||||||
:header-cell-style="{
|
:header-cell-style="{
|
||||||
background: '#F8FAFC',
|
background: 'var(--rr-table-header-bg, #F8FAFC)',
|
||||||
color: '#475569',
|
color: 'var(--rr-text-secondary, #475569)',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
fontSize: '12.5px',
|
fontSize: '12.5px',
|
||||||
padding: '10px 14px',
|
padding: '10px 14px',
|
||||||
borderBottom: '1px solid #E2E8F0',
|
borderBottom: '1px solid var(--rr-border-color, #E2E8F0)',
|
||||||
lineHeight: '18.75px',
|
lineHeight: '18.75px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
height: '40px'
|
height: '40px'
|
||||||
}"
|
}"
|
||||||
:cell-style="{
|
:cell-style="{
|
||||||
padding: '0 14px',
|
padding: '0 14px',
|
||||||
borderBottom: '1px solid #E2E8F0',
|
borderBottom: '1px solid var(--rr-border-color, #E2E8F0)',
|
||||||
fontSize: '13.5px',
|
fontSize: '13.5px',
|
||||||
lineHeight: '20.25px',
|
lineHeight: '20.25px',
|
||||||
color: '#0F172A',
|
color: 'var(--rr-text-primary, #0F172A)',
|
||||||
height: '52px'
|
height: '52px'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|||||||
117
src/components/ListTabs/ListTabs.vue
Normal file
117
src/components/ListTabs/ListTabs.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 通用 ListTabs 组件 (W17 设计规范整改)
|
||||||
|
*
|
||||||
|
* 状态 tab + 数字 badge, 完全按设计规范:
|
||||||
|
* 全部 128 待报价 23 进行中 45 已完成 52 已取消 8
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* <ListTabs
|
||||||
|
* v-model="activeTab"
|
||||||
|
* :items="[
|
||||||
|
* { key: 'all', label: '全部', count: 128 },
|
||||||
|
* { key: 'pending', label: '待报价', count: 23 },
|
||||||
|
* ...
|
||||||
|
* ]"
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
export interface ListTabItem {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: string
|
||||||
|
items: ListTabItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', v: string): void
|
||||||
|
(e: 'change', v: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function onClick(key: string) {
|
||||||
|
emit('update:modelValue', key)
|
||||||
|
emit('change', key)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-list-tabs">
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.key"
|
||||||
|
class="rr-list-tab"
|
||||||
|
:class="{ 'is-active': props.modelValue === item.key }"
|
||||||
|
@click="onClick(item.key)"
|
||||||
|
>
|
||||||
|
<span class="rr-list-tab__label">{{ item.label }}</span>
|
||||||
|
<span v-if="item.count !== undefined" class="rr-list-tab__count">{{ item.count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-list-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 32px;
|
||||||
|
padding: 0 0 12px 0;
|
||||||
|
border-bottom: 1px solid var(--rr-border-color, #E2E8F0);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.rr-list-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--rr-text-secondary, #475569);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -13px;
|
||||||
|
height: 2px;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--rr-color-primary, #2563EB);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: var(--rr-color-primary, #2563EB);
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: var(--rr-color-primary, #2563EB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--rr-text-tertiary, #94A3B8);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active &__count {
|
||||||
|
color: var(--rr-color-primary, #2563EB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 通用 PageHeader 组件 (W9 A2)
|
* F006 通用 PageHeader 组件 (W17 设计规范整改)
|
||||||
*
|
*
|
||||||
* 页面头: 标题 + 副标题 + 操作按钮区
|
* 页面头: 卡片化 (白底圆角阴影) + 标题 + 副标题 + 操作按钮区
|
||||||
*
|
*
|
||||||
* 用法:
|
* 用法:
|
||||||
* <PageHeader title="订单管理" :sub="`共 ${total} 条`">
|
* <PageHeader title="订单管理" :sub="`共 ${total} 条`">
|
||||||
@ -13,14 +13,16 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
title: string
|
title: string
|
||||||
sub?: string
|
sub?: string
|
||||||
|
bordered?: boolean
|
||||||
}
|
}
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
sub: ''
|
sub: '',
|
||||||
|
bordered: true
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page-header">
|
<div class="rr-page-header" :class="{ 'is-bordered': bordered }">
|
||||||
<div class="rr-page-header-text">
|
<div class="rr-page-header-text">
|
||||||
<h2 class="rr-page-title">{{ title }}</h2>
|
<h2 class="rr-page-title">{{ title }}</h2>
|
||||||
<p v-if="sub" class="rr-page-sub">{{ sub }}</p>
|
<p v-if="sub" class="rr-page-sub">{{ sub }}</p>
|
||||||
@ -33,26 +35,31 @@ withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page-header {
|
.rr-page-header {
|
||||||
margin-bottom: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: var(--rr-bg-card, #FFFFFF);
|
||||||
|
border: 1px solid var(--rr-border-color, #E2E8F0);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.rr-page-header-text {
|
.rr-page-header-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.rr-page-title {
|
.rr-page-title {
|
||||||
font-size: 22px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--rr-text-primary, #0F172A);
|
||||||
margin: 0 0 4px 0;
|
margin: 0 0 4px 0;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
.rr-page-sub {
|
.rr-page-sub {
|
||||||
color: var(--ink-3);
|
color: var(--rr-text-tertiary, #94A3B8);
|
||||||
font-size: 13px;
|
font-size: 12.5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 通用 SearchBar 组件 (W9 A2)
|
* F006 通用 SearchBar 组件 (W17 设计规范整改)
|
||||||
*
|
*
|
||||||
* 标准搜索条: 字段映射 + 行内表单 + 查询/重置按钮
|
* 标准搜索条: 紧凑布局 + 已选 chip 标签 + 查询/重置按钮
|
||||||
*
|
*
|
||||||
* 用法:
|
* 用法:
|
||||||
* <SearchBar
|
* <SearchBar
|
||||||
* v-model:filters="filters"
|
* v-model:filters="filters"
|
||||||
* :fields="[
|
* :fields="[
|
||||||
* { key: 'name', label: '名称', type: 'text', placeholder: '请输入' },
|
* { key: 'name', label: '名称', type: 'text', placeholder: '请输入' },
|
||||||
* { key: 'status', label: '状态', type: 'select', options: [
|
* { key: 'status', label: '状态', type: 'select', options: [...] },
|
||||||
* { value: 0, label: '启用' }, { value: 1, label: '禁用' }
|
|
||||||
* ] },
|
|
||||||
* { key: 'date', label: '日期', type: 'daterange' }
|
* { key: 'date', label: '日期', type: 'daterange' }
|
||||||
* ]"
|
* ]"
|
||||||
* @search="onSearch"
|
* @search="onSearch"
|
||||||
* @reset="onReset"
|
* @reset="onReset"
|
||||||
* />
|
* />
|
||||||
*
|
*
|
||||||
* 字段类型 (W9 阶段支持 5 种, 后续可扩):
|
* 字段类型:
|
||||||
* - text: el-input
|
* - text: el-input
|
||||||
* - select: el-select + options
|
* - select: el-select + options
|
||||||
* - date: el-date-picker (单日)
|
* - date: el-date-picker (单日)
|
||||||
@ -26,6 +24,7 @@
|
|||||||
* - number: el-input-number
|
* - number: el-input-number
|
||||||
*/
|
*/
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { Close } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
export interface SearchFieldOption {
|
export interface SearchFieldOption {
|
||||||
value: any
|
value: any
|
||||||
@ -46,10 +45,12 @@ interface Props {
|
|||||||
filters: Record<string, any>
|
filters: Record<string, any>
|
||||||
fields: SearchField[]
|
fields: SearchField[]
|
||||||
showReset?: boolean
|
showReset?: boolean
|
||||||
|
showChips?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
showReset: true
|
showReset: true,
|
||||||
|
showChips: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -64,6 +65,7 @@ const localFilters = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function onSearch() { emit('search') }
|
function onSearch() { emit('search') }
|
||||||
|
|
||||||
function onReset() {
|
function onReset() {
|
||||||
for (const f of props.fields) {
|
for (const f of props.fields) {
|
||||||
if (f.type === 'daterange') {
|
if (f.type === 'daterange') {
|
||||||
@ -76,23 +78,52 @@ function onReset() {
|
|||||||
}
|
}
|
||||||
emit('reset')
|
emit('reset')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnter() { onSearch() }
|
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] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取已选 chip
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-card rr-search">
|
<div class="rr-search-bar">
|
||||||
<el-form :inline="true" @keyup.enter="onEnter">
|
<div class="rr-search-bar__row">
|
||||||
<el-form-item
|
<div class="rr-search-bar__field" v-for="f in fields" :key="f.key">
|
||||||
v-for="f in fields"
|
<span class="rr-search-bar__label">{{ f.label }}</span>
|
||||||
:key="f.key"
|
|
||||||
:label="f.label"
|
|
||||||
>
|
|
||||||
<el-input
|
<el-input
|
||||||
v-if="f.type === 'text' || !f.type"
|
v-if="f.type === 'text' || !f.type"
|
||||||
v-model="localFilters[f.key]"
|
v-model="localFilters[f.key]"
|
||||||
:placeholder="f.placeholder || `请输入${f.label}`"
|
:placeholder="f.placeholder || `请输入${f.label}`"
|
||||||
:clearable="f.clearable !== false"
|
:clearable="f.clearable !== false"
|
||||||
:style="{ width: f.width || 220 }"
|
:style="{ width: f.width || 180 }"
|
||||||
|
@keyup.enter="onEnter"
|
||||||
/>
|
/>
|
||||||
<el-select
|
<el-select
|
||||||
v-else-if="f.type === 'select'"
|
v-else-if="f.type === 'select'"
|
||||||
@ -120,9 +151,9 @@ function onEnter() { onSearch() }
|
|||||||
v-else-if="f.type === 'daterange'"
|
v-else-if="f.type === 'daterange'"
|
||||||
v-model="localFilters[f.key]"
|
v-model="localFilters[f.key]"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
range-separator="至"
|
range-separator="~"
|
||||||
start-placeholder="开始"
|
:start-placeholder="f.placeholder || '开始日期'"
|
||||||
end-placeholder="结束"
|
:end-placeholder="'结束日期'"
|
||||||
:style="{ width: f.width || 260 }"
|
:style="{ width: f.width || 260 }"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
/>
|
/>
|
||||||
@ -132,16 +163,87 @@ function onEnter() { onSearch() }
|
|||||||
:placeholder="f.placeholder || `请输入${f.label}`"
|
:placeholder="f.placeholder || `请输入${f.label}`"
|
||||||
:style="{ width: f.width || 180 }"
|
:style="{ width: f.width || 180 }"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item>
|
<div class="rr-search-bar__actions">
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
<el-button v-if="showReset" @click="onReset">重置</el-button>
|
<el-button v-if="showReset" @click="onReset">重置</el-button>
|
||||||
</el-form-item>
|
</div>
|
||||||
</el-form>
|
</div>
|
||||||
|
<div v-if="showChips && activeChips.length" class="rr-search-bar__chips">
|
||||||
|
<span class="rr-search-bar__chips-label">已选:</span>
|
||||||
|
<el-tag
|
||||||
|
v-for="c in activeChips"
|
||||||
|
:key="c.key"
|
||||||
|
class="rr-search-bar__chip"
|
||||||
|
closable
|
||||||
|
:disable-transitions="true"
|
||||||
|
@close="onClearField(c.key)"
|
||||||
|
>
|
||||||
|
{{ c.label }}
|
||||||
|
</el-tag>
|
||||||
|
<el-link v-if="activeChips.length > 1" type="primary" :underline="false" @click="onReset" class="rr-search-bar__clear-all">
|
||||||
|
清空
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
.rr-search-bar {
|
||||||
.rr-search { padding: 16px 20px; }
|
padding: 16px 20px;
|
||||||
|
background: var(--rr-bg-card, #FFFFFF);
|
||||||
|
border: 1px solid var(--rr-border-color, #E2E8F0);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--rr-text-secondary, #475569);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px dashed var(--rr-border-color, #E2E8F0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--rr-text-tertiary, #94A3B8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chip {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__clear-all {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 用户管理 (前台用户列表) - W17 设计规范整改
|
* F006 用户管理 (前台用户列表) - W17 设计规范完整版
|
||||||
*
|
*
|
||||||
* W17: 9 列 + 6 搜索 + 4 操作,符合设计规范
|
* 9 列 + 6 搜索 + 4 操作, 符合设计规范:
|
||||||
* 走 /user/user/page 后端
|
* - 状态 chip (是否企业认证 / 禁用标识)
|
||||||
*
|
* - 操作列 el-link + 竖线分隔
|
||||||
* 字段 (从 API 实测):
|
* - 白卡片化表格
|
||||||
* id, nickName, mobile, header (头像 URL)
|
|
||||||
* isEnterprise (0/1), enterpriseName, accountBalance, enabled (0/1)
|
|
||||||
* createDate (ms)
|
|
||||||
* total=2284 真实用户
|
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
import { httpGet, httpPost } from '@/utils/request'
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
@ -188,7 +184,10 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<PageHeader title="用户列表" :sub="`前台注册用户 (共 ${total} 个)`" />
|
<PageHeader title="用户列表" :sub="`管理前台注册用户,共 ${total} 个账号`">
|
||||||
|
<el-button type="primary" :icon="'Plus'" @click="ElMessageBox.alert('导出功能待 W17 完善', '提示', { type: 'info' })">导出</el-button>
|
||||||
|
<el-button type="primary" @click="ElMessageBox.alert('新增用户功能待 W17 完善', '提示', { type: 'info' })">新增</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
<SearchBar
|
<SearchBar
|
||||||
v-model:filters="query"
|
v-model:filters="query"
|
||||||
@ -206,53 +205,52 @@ onMounted(load)
|
|||||||
@reset="onReset"
|
@reset="onReset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="rr-card">
|
<DataTable
|
||||||
<DataTable
|
:columns="columns"
|
||||||
:columns="columns"
|
:data="list"
|
||||||
:data="list"
|
:loading="loading"
|
||||||
:loading="loading"
|
:total="total"
|
||||||
:total="total"
|
:page="query.page"
|
||||||
:page="query.page"
|
:limit="query.limit"
|
||||||
:limit="query.limit"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:show-pagination="true"
|
||||||
:show-pagination="true"
|
:carded="true"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
empty-text="暂无用户"
|
empty-text="暂无用户"
|
||||||
@page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
>
|
>
|
||||||
<template #enabled="{ row }">
|
<template #enabled="{ row }">
|
||||||
<el-tag v-if="row.enabled === 1" type="success" size="small" effect="light">正常</el-tag>
|
<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">禁用</el-tag>
|
<el-tag v-else type="danger" size="small" effect="light" round>禁用</el-tag>
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<span class="rr-actions">
|
<span class="rr-actions">
|
||||||
<el-link type="primary" :underline="false" @click="onEdit(row)">修改</el-link>
|
<el-link type="primary" :underline="false" @click="onEdit(row)">修改</el-link>
|
||||||
<span class="rr-actions__sep">|</span>
|
<span class="rr-actions__sep">|</span>
|
||||||
<el-link :type="row.enabled === 1 ? 'warning' : 'success'" :underline="false" @click="toggleEnabled(row)">
|
<el-link :type="row.enabled === 1 ? 'warning' : 'success'" :underline="false" @click="toggleEnabled(row)">
|
||||||
{{ row.enabled === 1 ? '禁用' : '启用' }}
|
{{ row.enabled === 1 ? '禁用' : '启用' }}
|
||||||
</el-link>
|
</el-link>
|
||||||
<span class="rr-actions__sep">|</span>
|
<span class="rr-actions__sep">|</span>
|
||||||
<el-link type="danger" :underline="false" @click="deleteUser(row)">删除</el-link>
|
<el-link type="danger" :underline="false" @click="deleteUser(row)">删除</el-link>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1600px; }
|
.rr-page { max-width: 100%; }
|
||||||
.user-header { width: 32px; height: 32px; border-radius: 50%; vertical-align: middle; }
|
.user-header { width: 32px; height: 32px; border-radius: 50%; vertical-align: middle; }
|
||||||
.rr-actions {
|
.rr-actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
&__sep {
|
&__sep {
|
||||||
color: var(--rr-border, #e2e8f0);
|
color: var(--rr-border-color, #E2E8F0);
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.el-link {
|
:deep(.el-link) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user