Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6033c65d5 | |||
| c2b3473c5e | |||
| 30cc69f43b | |||
| 092d1991c7 | |||
| 0519de9428 | |||
| 4c27e6b4b2 | |||
| 90d6561746 | |||
| 99bc522b24 | |||
| 508c84e571 | |||
| 8be7ca9dc2 | |||
| fc7d9f9179 |
18
package-lock.json
generated
18
package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^26.2.0",
|
||||||
"@types/qs": "^6.15.1",
|
"@types/qs": "^6.15.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
@ -1280,6 +1281,16 @@
|
|||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "26.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
|
||||||
|
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~8.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.15.1",
|
"version": "6.15.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz",
|
||||||
@ -2913,6 +2924,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unimport": {
|
"node_modules/unimport": {
|
||||||
"version": "3.14.6",
|
"version": "3.14.6",
|
||||||
"resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz",
|
"resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz",
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^26.2.0",
|
||||||
"@types/qs": "^6.15.1",
|
"@types/qs": "^6.15.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
|
|||||||
@ -63,7 +63,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
pageSizes: () => [10, 20, 50, 100],
|
pageSizes: () => [10, 20, 50, 100],
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
stripe: false,
|
stripe: false,
|
||||||
borderless: false,
|
// 2026-08-18 UI 统一: 默认无边框 (带 border 的老页面由全局样式消除, 见 styles/global-ui.scss)
|
||||||
|
borderless: true,
|
||||||
emptyText: '暂无数据',
|
emptyText: '暂无数据',
|
||||||
errorText: '',
|
errorText: '',
|
||||||
error: null
|
error: null
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import 'element-plus/dist/index.css'
|
|||||||
import './theme/tokens.scss'
|
import './theme/tokens.scss'
|
||||||
import './theme/themes.scss'
|
import './theme/themes.scss'
|
||||||
import './styles/global-popper.scss'
|
import './styles/global-popper.scss'
|
||||||
|
import './styles/global-ui.scss' // 2026-08-18 UI 统一规范 (表格/卡片/内边距/暗色状态色)
|
||||||
import { loadTheme } from './theme/presets'
|
import { loadTheme } from './theme/presets'
|
||||||
|
|
||||||
// 启动时恢复用户主题
|
// 启动时恢复用户主题
|
||||||
|
|||||||
@ -414,7 +414,7 @@ window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.res
|
|||||||
.rr-dashboard { max-width: 1400px; }
|
.rr-dashboard { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 20px; }
|
.rr-page-header { margin-bottom: 20px; }
|
||||||
.rr-page-title {
|
.rr-page-title {
|
||||||
font-size: 22px; font-weight: 600; letter-spacing: -0.02em;
|
font-size: 20px; font-weight: 700;
|
||||||
color: var(--ink); margin-bottom: 4px;
|
color: var(--ink); margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
||||||
|
|||||||
@ -192,15 +192,19 @@ onMounted(() => { load() })
|
|||||||
|
|
||||||
/* 状态徽章(4 种) - 浅底色块(套设计规范 §5.4) */
|
/* 状态徽章(4 种) - 浅底色块(套设计规范 §5.4) */
|
||||||
.rr-badge {
|
.rr-badge {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
padding: 2px 8px;
|
align-items: center;
|
||||||
border-radius: 4px;
|
gap: 6px;
|
||||||
font-size: 11.5px;
|
padding: 2px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 17.25px;
|
line-height: 20px;
|
||||||
height: 21.25px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
.rr-badge::before {
|
||||||
|
content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor;
|
||||||
|
}
|
||||||
.rr-badge-pending { background: var(--amber-soft); color: var(--amber); }
|
.rr-badge-pending { background: var(--amber-soft); color: var(--amber); }
|
||||||
.rr-badge-quoting { background: var(--brand-soft); color: var(--brand); }
|
.rr-badge-quoting { background: var(--brand-soft); color: var(--brand); }
|
||||||
.rr-badge-done { background: var(--green-soft); color: var(--green); }
|
.rr-badge-done { background: var(--green-soft); color: var(--green); }
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
import { ref, onMounted, reactive, computed } from 'vue'
|
import { ref, onMounted, reactive, computed } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Clock } from '@element-plus/icons-vue'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
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'
|
||||||
@ -46,6 +47,14 @@ interface Order {
|
|||||||
const STATUS_LABEL: Record<number, string> = {
|
const STATUS_LABEL: Record<number, string> = {
|
||||||
1: '待支付', 2: '待发货', 3: '待收货', 4: '待付尾款', 5: '待评价', 6: '已完成', 7: '已取消', 8: '采购单待审核', 9: '付款单待审核'
|
1: '待支付', 2: '待发货', 3: '待收货', 4: '待付尾款', 5: '待评价', 6: '已完成', 7: '已取消', 8: '采购单待审核', 9: '付款单待审核'
|
||||||
}
|
}
|
||||||
|
// 状态胶囊配色 (已完成=绿/已取消=灰/其余=品牌蓝, 引用全局 --st-* 变量暗色自适应)
|
||||||
|
const STATUS_PILL: Record<number, { color: string; bg: string }> = {
|
||||||
|
6: { color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
|
7: { color: 'var(--st-lv4-fg)', bg: 'var(--st-lv4-bg)' }
|
||||||
|
}
|
||||||
|
function statusPill(status: number) {
|
||||||
|
return STATUS_PILL[status] || { color: 'var(--primary)', bg: 'var(--primary-soft, #dbeafe)' }
|
||||||
|
}
|
||||||
|
|
||||||
const STATUS_TABS = [
|
const STATUS_TABS = [
|
||||||
{ value: '', label: '全部' },
|
{ value: '', label: '全部' },
|
||||||
@ -259,19 +268,15 @@ onMounted(load)
|
|||||||
<span v-else class="rr-muted">--</span>
|
<span v-else class="rr-muted">--</span>
|
||||||
</template>
|
</template>
|
||||||
<template #time="{ row }">
|
<template #time="{ row }">
|
||||||
<span class="rr-time">{{ fmtDate(row.createDate) }}</span>
|
<span class="rr-time"><el-icon style="vertical-align:-2px;margin-right:4px"><Clock /></el-icon>{{ fmtDate(row.createDate) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #amount="{ row }">
|
<template #amount="{ row }">
|
||||||
<span class="rr-amount">¥ {{ row.totalAmount.toLocaleString() }}</span>
|
<span class="rr-amount">¥ {{ row.totalAmount.toLocaleString() }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #status="{ row }">
|
<template #status="{ row }">
|
||||||
<el-tag
|
<span class="st-pill" :style="{ color: statusPill(row.status).color, background: statusPill(row.status).bg }">
|
||||||
:type="(STATUS_LABEL[row.status] === '已完成' ? 'success' : STATUS_LABEL[row.status] === '已取消' ? 'info' : 'primary') as any"
|
<span class="st-dot"></span>{{ STATUS_LABEL[row.status] || `状态${row.status}` }}
|
||||||
size="small"
|
</span>
|
||||||
effect="light"
|
|
||||||
>
|
|
||||||
{{ STATUS_LABEL[row.status] || `状态${row.status}` }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<TableActions
|
<TableActions
|
||||||
|
|||||||
@ -29,9 +29,9 @@ interface Groupbuy {
|
|||||||
raw: any
|
raw: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
const STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
1: { label: '启用', type: 'success' },
|
1: { label: '启用', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
2: { label: '停用', type: 'danger' }
|
2: { label: '停用', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK: Groupbuy[] = [
|
const FALLBACK: Groupbuy[] = [
|
||||||
@ -116,6 +116,33 @@ onMounted(load)
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
|
<div class="batch-bar" :class="{ 'has-selection': selection.length > 0 }">
|
||||||
|
<span v-if="selection.length > 0" class="batch-count">已选 <strong>{{ selection.length }}</strong> 个活动</span>
|
||||||
|
<span v-else class="batch-hint">勾选活动后可批量操作</span>
|
||||||
|
<span class="batch-actions">
|
||||||
|
<el-button size="small" type="danger" plain :disabled="!selection.length" @click="onBatchDelete">批量删除</el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-table :data="list" v-loading="loading" style="width: 100%" @selection-change="onSelectionChange" row-key="id" empty-text="暂无拼团活动">
|
||||||
|
<el-table-column type="selection" width="48" align="center" />
|
||||||
|
<el-table-column prop="name" label="活动名称" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="enterpriseName" label="活动商家" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="groupBeginTime" label="开始时间" width="150" />
|
||||||
|
<el-table-column prop="groupEndTime" label="结束时间" width="150" />
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="st-pill" :style="{ color: STATUS_MAP[row.status]?.color, background: STATUS_MAP[row.status]?.bg }"><span class="st-dot"></span>{{ STATUS_MAP[row.status]?.label }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createDate" label="创建时间" width="165" />
|
||||||
|
<el-table-column label="操作" width="170" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" text @click="onView(row)">详情</el-button>
|
||||||
|
<el-button :type="row.status === 1 ? 'warning' : 'success'" size="small" text @click="onToggleStatus(row)">{{ row.status === 1 ? '停用' : '启用' }}</el-button>
|
||||||
|
<el-button type="danger" size="small" text @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
<div class="rr-pagination">
|
<div class="rr-pagination">
|
||||||
<el-pagination :current-page="page" :page-sizes="[10, 20, 50, 100]" :page-size="limit" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="onLimitChange" @current-change="onPageChange" />
|
<el-pagination :current-page="page" :page-sizes="[10, 20, 50, 100]" :page-size="limit" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="onLimitChange" @current-change="onPageChange" />
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +156,7 @@ onMounted(load)
|
|||||||
<el-descriptions-item label="结束时间">{{ viewData.groupEndTime }}</el-descriptions-item>
|
<el-descriptions-item label="结束时间">{{ viewData.groupEndTime }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="活动商品" :span="2">{{ viewData.type }}</el-descriptions-item>
|
<el-descriptions-item label="活动商品" :span="2">{{ viewData.type }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="(STATUS_MAP[viewData.status]?.type as any)" size="small">{{ STATUS_MAP[viewData.status]?.label }}</el-tag>
|
<span class="st-pill" :style="{ color: STATUS_MAP[viewData.status]?.color, background: STATUS_MAP[viewData.status]?.bg }"><span class="st-dot"></span>{{ STATUS_MAP[viewData.status]?.label }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ viewData.createDate }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ viewData.createDate }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 资讯列表 - P1 真 view
|
* F006 资讯列表 - 真 view + 发布功能 (2026-08-08 增强)
|
||||||
*
|
*
|
||||||
* 走 /other/information/page
|
* 列表: GET /other/information/page
|
||||||
* 真实接口 (2026-08-07 探测: 200, total=7)
|
* 新增: POST /other/information/save (title/categoryId/categoryName/content/images)
|
||||||
* 字段: id/title/categoryId/categoryName/content/createTime
|
* 修改: POST /other/information/update
|
||||||
|
* 删除: POST /other/information/delete (body {id})
|
||||||
|
* 分类: GET /other/category/list?type=3&level=1 (一级分类, 排除无 level 的脏数据)
|
||||||
|
* 图片: POST /sys/file/upload (FormData, 返 data.url) — 参考旧系统 information-add-or-update.vue
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive, computed } from 'vue'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet, httpPost, httpDelete } from '@/utils/request'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
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'
|
||||||
|
|
||||||
@ -16,6 +20,8 @@ interface Info {
|
|||||||
title: string
|
title: string
|
||||||
categoryId: string
|
categoryId: string
|
||||||
categoryName: string
|
categoryName: string
|
||||||
|
content: string
|
||||||
|
images: string
|
||||||
createTime: string
|
createTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,39 +29,233 @@ const list = ref<Info[]>([])
|
|||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const query = reactive({ page: 1, limit: 10 })
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
const search = reactive({ title: '' })
|
||||||
|
|
||||||
|
const categories = ref<{ id: string; name: string }[]>([])
|
||||||
|
const categoryLoading = ref(false)
|
||||||
|
|
||||||
|
// === 弹窗状态 ===
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editMode = ref<'create' | 'update'>('create')
|
||||||
|
const saving = ref(false)
|
||||||
|
const uploadLoading = ref(false)
|
||||||
|
const editForm = reactive({
|
||||||
|
id: '' as string,
|
||||||
|
title: '',
|
||||||
|
categoryId: '' as string,
|
||||||
|
categoryName: '',
|
||||||
|
content: '',
|
||||||
|
images: '',
|
||||||
|
})
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ prop: 'id', label: 'ID', width: 90 },
|
{ prop: 'id', label: 'ID', width: 90 },
|
||||||
{ prop: 'title', label: '资讯标题', minWidth: 280 },
|
{ prop: 'title', label: '资讯标题', minWidth: 260 },
|
||||||
{ prop: 'categoryName', label: '分类', width: 140, formatter: (r: Info) => r.categoryName || '--' },
|
{ prop: 'categoryName', label: '分类', width: 120, formatter: (r: Info) => r.categoryName || '--' },
|
||||||
{ prop: 'createTime', label: '发布时间', width: 180 }
|
{ prop: 'createTime', label: '发布时间', width: 170 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const imgStyle = computed(() => ({
|
||||||
|
maxWidth: '140px',
|
||||||
|
maxHeight: '48px',
|
||||||
|
objectFit: 'cover' as const,
|
||||||
|
borderRadius: '4px',
|
||||||
|
}))
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const resp: any = await httpGet('/other/information/page', { page: query.page, limit: query.limit })
|
const params: any = { page: query.page, limit: query.limit }
|
||||||
|
if (search.title.trim()) params.title = search.title.trim()
|
||||||
|
const resp: any = await httpGet('/other/information/page', params)
|
||||||
list.value = resp?.list || []
|
list.value = resp?.list || []
|
||||||
total.value = Number(resp?.total || 0)
|
total.value = Number(resp?.total || 0)
|
||||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
}
|
}
|
||||||
function onPageChange(p: number) { query.page = p; load() }
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
onMounted(load)
|
function onSearch() { query.page = 1; load() }
|
||||||
|
function onReset() { search.title = ''; query.page = 1; load() }
|
||||||
|
|
||||||
|
// === 分类 ===
|
||||||
|
async function loadCategories() {
|
||||||
|
categoryLoading.value = true
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet('/other/category/list', { type: 3, level: 1 })
|
||||||
|
categories.value = (data || [])
|
||||||
|
.filter((c: any) => c.level === 1 && c.id)
|
||||||
|
.map((c: any) => ({ id: String(c.id), name: c.name }))
|
||||||
|
.sort((a: any, b: any) => (a.name > b.name ? 1 : -1))
|
||||||
|
} catch { categories.value = [] } finally { categoryLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 新增/编辑 ===
|
||||||
|
function openCreate() {
|
||||||
|
editMode.value = 'create'
|
||||||
|
editForm.id = ''; editForm.title = ''; editForm.categoryId = ''; editForm.categoryName = ''; editForm.content = ''; editForm.images = ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
loadCategories()
|
||||||
|
}
|
||||||
|
function openEdit(row: Info) {
|
||||||
|
editMode.value = 'update'
|
||||||
|
editForm.id = String(row.id)
|
||||||
|
editForm.title = row.title || ''
|
||||||
|
editForm.categoryId = row.categoryId ? String(row.categoryId) : ''
|
||||||
|
editForm.categoryName = row.categoryName || ''
|
||||||
|
editForm.content = row.content || ''
|
||||||
|
editForm.images = row.images || ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
loadCategories()
|
||||||
|
}
|
||||||
|
function onCategoryChange(id: string) {
|
||||||
|
const c = categories.value.find(x => x.id === id)
|
||||||
|
editForm.categoryName = c?.name || ''
|
||||||
|
}
|
||||||
|
async function submit() {
|
||||||
|
if (!editForm.title.trim()) { ElMessage.warning('请填写标题'); return }
|
||||||
|
if (!editForm.categoryId) { ElMessage.warning('请选择分类'); return }
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
title: editForm.title.trim(),
|
||||||
|
categoryId: editForm.categoryId,
|
||||||
|
categoryName: editForm.categoryName,
|
||||||
|
content: editForm.content,
|
||||||
|
images: editForm.images,
|
||||||
|
}
|
||||||
|
if (editMode.value === 'create') {
|
||||||
|
await httpPost('/other/information/save', payload)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
} else {
|
||||||
|
payload.id = editForm.id
|
||||||
|
await httpPost('/other/information/update', payload)
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '保存失败')
|
||||||
|
} finally { saving.value = false }
|
||||||
|
}
|
||||||
|
async function onDelete(row: Info) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除资讯「${row.title}」吗?`, '提示', { type: 'warning' })
|
||||||
|
await httpDelete('/other/information/delete', { id: row.id })
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e === 'cancel' || e === 'close') return
|
||||||
|
ElMessage.error(e?.msg || e?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 图片上传 (FormData + Bearer 头由拦截器自动带) ===
|
||||||
|
async function customUpload(opt: any) {
|
||||||
|
const file = opt.file as File
|
||||||
|
if (file.size > 2 * 1024 * 1024) { ElMessage.error('图片大小不能超过 2MB!'); return }
|
||||||
|
uploadLoading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
const { http } = await import('@/utils/request')
|
||||||
|
const res: any = await http.post('/sys/file/upload', fd, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
const body = res?.data
|
||||||
|
if (body?.code === 0 && body?.data?.url) {
|
||||||
|
editForm.images = body.data.url
|
||||||
|
ElMessage.success('图片上传成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('图片上传失败: ' + (body?.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error('图片上传失败: ' + (e?.message || '网络错误'))
|
||||||
|
} finally { uploadLoading.value = false }
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<PageHeader title="资讯列表" :sub="`平台资讯 (共 ${total} 条)`" />
|
<PageHeader title="资讯列表" :sub="`平台资讯 (共 ${total} 条)`">
|
||||||
|
<el-button type="primary" @click="openCreate()">新增资讯</el-button>
|
||||||
|
</PageHeader>
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
|
<div class="rr-filters-row" style="padding: 16px 16px 0;">
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="search.title" placeholder="搜索标题" clearable style="width: 220px" @keyup.enter="onSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
|
<el-button @click="onReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
<DataTable :columns="columns" :data="list"
|
<DataTable :columns="columns" :data="list"
|
||||||
:borderless="true" :loading="loading" :total="total" :page="query.page" :limit="query.limit" :show-pagination="true" row-key="id" empty-text="暂无资讯" @page-change="onPageChange"
|
:borderless="true" :loading="loading" :total="total" :page="query.page" :limit="query.limit" :show-pagination="true" row-key="id" empty-text="暂无资讯" @page-change="onPageChange"
|
||||||
@limit-change="onLimitChange" />
|
@limit-change="onLimitChange">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-table-column label="封面" width="160">
|
||||||
|
<template #default="{ row: r }">
|
||||||
|
<img v-if="r.images" :src="r.images" :style="imgStyle" alt="封面" referrerpolicy="no-referrer" />
|
||||||
|
<span v-else class="rr-muted">无图</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row: r }">
|
||||||
|
<span class="rr-actions">
|
||||||
|
<el-link type="primary" @click="openEdit(r)">编辑</el-link>
|
||||||
|
<span class="rr-actions__sep">|</span>
|
||||||
|
<el-link type="danger" @click="onDelete(r)">删除</el-link>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="editMode === 'create' ? '新增资讯' : '编辑资讯'" width="640px" destroy-on-close>
|
||||||
|
<el-form label-width="90px" label-position="right">
|
||||||
|
<el-form-item label="标题" required>
|
||||||
|
<el-input v-model="editForm.title" placeholder="资讯标题" maxlength="50" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="一级分类" required>
|
||||||
|
<el-select v-model="editForm.categoryId" placeholder="请选择一级分类" :loading="categoryLoading" filterable style="width: 100%" @change="onCategoryChange">
|
||||||
|
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="封面图片">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<el-upload
|
||||||
|
:show-file-list="false"
|
||||||
|
accept="image/jpeg,image/png,image/jpg"
|
||||||
|
:http-request="customUpload"
|
||||||
|
:disabled="uploadLoading">
|
||||||
|
<img v-if="editForm.images" :src="editForm.images" alt="封面" class="upload-preview" referrerpolicy="no-referrer" />
|
||||||
|
<el-button v-else :loading="uploadLoading">上传封面</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<span class="rr-muted">支持 JPG/PNG, 不超过 2MB</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="资讯内容">
|
||||||
|
<el-input v-model="editForm.content" type="textarea" :rows="8" placeholder="资讯内容(支持 HTML)" maxlength="20000" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
</style>
|
.rr-filters-row { display: flex; }
|
||||||
|
.rr-muted { color: var(--text-muted, #909399); font-size: 12px; }
|
||||||
|
.rr-actions { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
|
.rr-actions__sep { color: var(--border, #e4e7ed); }
|
||||||
|
.upload-preview { max-width: 160px; max-height: 80px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border, #e4e7ed); }
|
||||||
|
</style>
|
||||||
|
|||||||
@ -29,9 +29,9 @@ interface Seckill {
|
|||||||
raw: any
|
raw: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
const STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
0: { label: '启用', type: 'success' },
|
0: { label: '启用', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
1: { label: '停用', type: 'danger' }
|
1: { label: '停用', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK: Seckill[] = [
|
const FALLBACK: Seckill[] = [
|
||||||
@ -117,6 +117,33 @@ onMounted(load)
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
|
<div class="batch-bar" :class="{ 'has-selection': selection.length > 0 }">
|
||||||
|
<span v-if="selection.length > 0" class="batch-count">已选 <strong>{{ selection.length }}</strong> 个活动</span>
|
||||||
|
<span v-else class="batch-hint">勾选活动后可批量操作</span>
|
||||||
|
<span class="batch-actions">
|
||||||
|
<el-button size="small" type="danger" plain :disabled="!selection.length" @click="onBatchDelete">批量删除</el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-table :data="list" v-loading="loading" style="width: 100%" @selection-change="onSelectionChange" row-key="id" empty-text="暂无秒杀活动">
|
||||||
|
<el-table-column type="selection" width="48" align="center" />
|
||||||
|
<el-table-column prop="name" label="活动名称" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="enterpriseName" label="活动商家" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="beginTime" label="开始时间" width="150" />
|
||||||
|
<el-table-column prop="endTime" label="结束时间" width="150" />
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="st-pill" :style="{ color: STATUS_MAP[row.status]?.color, background: STATUS_MAP[row.status]?.bg }"><span class="st-dot"></span>{{ STATUS_MAP[row.status]?.label }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createDate" label="创建时间" width="165" />
|
||||||
|
<el-table-column label="操作" width="170" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" text @click="onView(row)">详情</el-button>
|
||||||
|
<el-button :type="row.status === 0 ? 'warning' : 'success'" size="small" text @click="onToggleStatus(row)">{{ row.status === 0 ? '停用' : '启用' }}</el-button>
|
||||||
|
<el-button type="danger" size="small" text @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
<div class="rr-pagination">
|
<div class="rr-pagination">
|
||||||
<el-pagination :current-page="page" :page-sizes="[10, 20, 50, 100]" :page-size="limit" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="onLimitChange" @current-change="onPageChange" />
|
<el-pagination :current-page="page" :page-sizes="[10, 20, 50, 100]" :page-size="limit" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="onLimitChange" @current-change="onPageChange" />
|
||||||
</div>
|
</div>
|
||||||
@ -130,7 +157,7 @@ onMounted(load)
|
|||||||
<el-descriptions-item label="结束时间">{{ viewData.endTime }}</el-descriptions-item>
|
<el-descriptions-item label="结束时间">{{ viewData.endTime }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="活动商品" :span="2">{{ viewData.type }}</el-descriptions-item>
|
<el-descriptions-item label="活动商品" :span="2">{{ viewData.type }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="(STATUS_MAP[viewData.status]?.type as any)" size="small">{{ STATUS_MAP[viewData.status]?.label }}</el-tag>
|
<span class="st-pill" :style="{ color: STATUS_MAP[viewData.status]?.color, background: STATUS_MAP[viewData.status]?.bg }"><span class="st-dot"></span>{{ STATUS_MAP[viewData.status]?.label }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ viewData.createDate }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ viewData.createDate }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|||||||
@ -33,10 +33,10 @@ interface Brand {
|
|||||||
raw: any
|
raw: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
const STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
1: { label: '待审核', type: 'warning' },
|
1: { label: '待审核', color: 'var(--st-warn-fg)', bg: 'var(--st-warn-bg)' },
|
||||||
2: { label: '审核通过', type: 'success' },
|
2: { label: '审核通过', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
3: { label: '审核拒绝', type: 'danger' }
|
3: { label: '审核拒绝', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = ref<Brand[]>([])
|
const list = ref<Brand[]>([])
|
||||||
@ -212,7 +212,7 @@ onMounted(load)
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="审核状态" header-align="center" align="center" width="100">
|
<el-table-column prop="status" label="审核状态" header-align="center" align="center" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="STATUS_MAP[row.status]?.type || 'info'" size="small">{{ STATUS_MAP[row.status]?.label || row.status }}</el-tag>
|
<span class="st-pill" :style="{ color: STATUS_MAP[row.status]?.color || 'var(--st-lv4-fg)', background: STATUS_MAP[row.status]?.bg || 'var(--st-lv4-bg)' }"><span class="st-dot"></span>{{ STATUS_MAP[row.status]?.label || row.status }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createDate" label="申请时间" header-align="center" align="center" width="170" show-overflow-tooltip />
|
<el-table-column prop="createDate" label="申请时间" header-align="center" align="center" width="170" show-overflow-tooltip />
|
||||||
@ -241,7 +241,7 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="auditVisible" title="品牌审核" width="440px" :close-on-click-modal="false">
|
<el-dialog v-model="auditVisible" title="品牌审核" width="480px" :close-on-click-modal="false">
|
||||||
<el-form label-width="80px">
|
<el-form label-width="80px">
|
||||||
<el-form-item label="审核结果">
|
<el-form-item label="审核结果">
|
||||||
<el-radio-group v-model="auditForm.status">
|
<el-radio-group v-model="auditForm.status">
|
||||||
@ -265,7 +265,7 @@ onMounted(load)
|
|||||||
<el-table-column prop="sourceEnterpriseName" label="品牌来源" header-align="center" align="center" min-width="140" show-overflow-tooltip />
|
<el-table-column prop="sourceEnterpriseName" label="品牌来源" header-align="center" align="center" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column prop="status" label="审核状态" header-align="center" align="center" width="100">
|
<el-table-column prop="status" label="审核状态" header-align="center" align="center" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="STATUS_MAP[row.status]?.type || 'info'" size="small">{{ STATUS_MAP[row.status]?.label || row.status }}</el-tag>
|
<span class="st-pill" :style="{ color: STATUS_MAP[row.status]?.color || 'var(--st-lv4-fg)', background: STATUS_MAP[row.status]?.bg || 'var(--st-lv4-bg)' }"><span class="st-dot"></span>{{ STATUS_MAP[row.status]?.label || row.status }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createDate" label="申请时间" header-align="center" align="center" width="170" />
|
<el-table-column prop="createDate" label="申请时间" header-align="center" align="center" width="170" />
|
||||||
@ -279,7 +279,7 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1500px; }
|
.rr-page { max-width: 1500px; }
|
||||||
.rr-card { background: var(--el-bg-color); border-radius: 8px; border: 1px solid var(--el-border-color-light); overflow: hidden; }
|
.rr-card { background: var(--el-bg-color); border-radius: 12px; border: 1px solid var(--el-border-color-light); overflow: hidden; }
|
||||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--el-border-color-light); }
|
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--el-border-color-light); }
|
||||||
.brand-auth { width: 50px; height: 50px; border-radius: 4px; border: 1px solid var(--el-border-color-light); cursor: pointer; }
|
.brand-auth { width: 50px; height: 50px; border-radius: 4px; border: 1px solid var(--el-border-color-light); cursor: pointer; }
|
||||||
.rr-muted { color: var(--el-text-color-placeholder); }
|
.rr-muted { color: var(--el-text-color-placeholder); }
|
||||||
|
|||||||
@ -15,16 +15,13 @@
|
|||||||
*
|
*
|
||||||
* 失败回退: FALLBACK_*
|
* 失败回退: FALLBACK_*
|
||||||
*/
|
*/
|
||||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
import { ref, onMounted, defineAsyncComponent } from 'vue'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
import * as echarts from 'echarts/core'
|
|
||||||
import { LineChart, PieChart } from 'echarts/charts'
|
|
||||||
import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
|
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
|
||||||
import { useThemeStore } from '@/stores/theme'
|
|
||||||
|
|
||||||
echarts.use([LineChart, PieChart, TooltipComponent, GridComponent, LegendComponent, CanvasRenderer])
|
// 图表组件: echarts 在组件内动态 import, 独立 chunk 懒加载 (2026-08-18)
|
||||||
|
const TrendChart = defineAsyncComponent(() => import('./charts/EchartsTrendChart.vue'))
|
||||||
|
const StatusPie = defineAsyncComponent(() => import('./charts/EchartsStatusPie.vue'))
|
||||||
|
|
||||||
interface OrderItem {
|
interface OrderItem {
|
||||||
orderId: string
|
orderId: string
|
||||||
@ -51,12 +48,6 @@ const recentOrders = ref<any[]>([])
|
|||||||
const trendData = ref<{ dates: string[]; counts: number[]; amounts: number[] }>({ dates: [], counts: [], amounts: [] })
|
const trendData = ref<{ dates: string[]; counts: number[]; amounts: number[] }>({ dates: [], counts: [], amounts: [] })
|
||||||
const statusSlices = ref<{ name: string; value: number; color: string }[]>([])
|
const statusSlices = ref<{ name: string; value: number; color: string }[]>([])
|
||||||
const apiStatus = ref<'loading' | 'live' | 'fallback'>('loading')
|
const apiStatus = ref<'loading' | 'live' | 'fallback'>('loading')
|
||||||
const trendChartEl = ref<HTMLDivElement | null>(null)
|
|
||||||
const statusChartEl = ref<HTMLDivElement | null>(null)
|
|
||||||
let trendChart: echarts.ECharts | null = null
|
|
||||||
let statusChart: echarts.ECharts | null = null
|
|
||||||
const themeStore = useThemeStore()
|
|
||||||
|
|
||||||
// 订单状态映射 (老 renren: 1=待付款 2=已付款 3=已发货 4=已完成 5=已取消)
|
// 订单状态映射 (老 renren: 1=待付款 2=已付款 3=已发货 4=已完成 5=已取消)
|
||||||
const ORDER_STATUS: Record<number, { label: string; type: string }> = {
|
const ORDER_STATUS: Record<number, { label: string; type: string }> = {
|
||||||
1: { label: '待付款', type: 'warning' },
|
1: { label: '待付款', type: 'warning' },
|
||||||
@ -213,73 +204,8 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
apiStatus.value = isLive ? 'live' : 'fallback'
|
apiStatus.value = isLive ? 'live' : 'fallback'
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
renderTrend()
|
|
||||||
renderStatus()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ECharts 渲染
|
|
||||||
function getBrand(): string { return getComputedStyle(document.body).getPropertyValue('--brand').trim() || '#2563EB' }
|
|
||||||
function getInk(): string { return getComputedStyle(document.body).getPropertyValue('--ink-3').trim() || '#64748B' }
|
|
||||||
function getBorder(): string { return getComputedStyle(document.body).getPropertyValue('--border').trim() || '#E2E8F0' }
|
|
||||||
function getSurface(): string { return getComputedStyle(document.body).getPropertyValue('--surface').trim() || '#FFFFFF' }
|
|
||||||
function getChartColors(): string[] {
|
|
||||||
return ['--chart-1', '--chart-2', '--chart-3', '--chart-4', '--red'].map(v =>
|
|
||||||
getComputedStyle(document.body).getPropertyValue(v).trim()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTrend() {
|
|
||||||
if (!trendChartEl.value) return
|
|
||||||
if (!trendChart) trendChart = echarts.init(trendChartEl.value)
|
|
||||||
const ink = getInk(), border = getBorder(), surface = getSurface(), brand = getBrand()
|
|
||||||
const chart2 = getComputedStyle(document.body).getPropertyValue('--chart-2').trim() || '#7C3AED'
|
|
||||||
trendChart.setOption({
|
|
||||||
grid: { left: 50, right: 50, top: 40, bottom: 30 },
|
|
||||||
tooltip: { trigger: 'axis', backgroundColor: surface, borderColor: border, textStyle: { color: ink } },
|
|
||||||
legend: { top: 0, textStyle: { color: ink } },
|
|
||||||
xAxis: { type: 'category', data: trendData.value.dates, axisLine: { lineStyle: { color: border } }, axisLabel: { color: ink } },
|
|
||||||
yAxis: [
|
|
||||||
{ type: 'value', name: '订单数', position: 'left', splitLine: { lineStyle: { color: border } }, axisLabel: { color: ink } },
|
|
||||||
{ type: 'value', name: '交易额', position: 'right', splitLine: { show: false }, axisLabel: { color: ink, formatter: (v: number) => v >= 1000 ? `${(v/1000).toFixed(0)}K` : String(v) } }
|
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{ name: '订单数', type: 'line', data: trendData.value.counts, smooth: true, itemStyle: { color: brand }, lineStyle: { color: brand, width: 2 }, areaStyle: { color: brand, opacity: 0.15 } },
|
|
||||||
{ name: '交易额', type: 'line', yAxisIndex: 1, data: trendData.value.amounts, smooth: true, itemStyle: { color: chart2 }, lineStyle: { color: chart2, width: 2 } }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStatus() {
|
|
||||||
if (!statusChartEl.value) return
|
|
||||||
if (!statusChart) statusChart = echarts.init(statusChartEl.value)
|
|
||||||
const ink = getInk(), border = getBorder(), surface = getSurface()
|
|
||||||
const colors = getChartColors()
|
|
||||||
const total = statusSlices.value.reduce((a, b) => a + b.value, 0)
|
|
||||||
statusChart.setOption({
|
|
||||||
tooltip: { trigger: 'item', backgroundColor: surface, borderColor: border, textStyle: { color: ink } },
|
|
||||||
legend: { bottom: 0, textStyle: { color: ink } },
|
|
||||||
series: [{
|
|
||||||
type: 'pie', radius: ['58%', '80%'], center: ['50%', '45%'],
|
|
||||||
avoidLabelOverlap: true,
|
|
||||||
label: { show: false }, labelLine: { show: false },
|
|
||||||
data: statusSlices.value.map((s, i) => ({ name: s.name, value: s.value, itemStyle: { color: colors[i] } })),
|
|
||||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 600, color: ink } }
|
|
||||||
}],
|
|
||||||
graphic: total > 0 ? [
|
|
||||||
{ type: 'text', left: 'center', top: '38%', style: { text: '总计', fontSize: 10, fill: ink, opacity: 0.5 } },
|
|
||||||
{ type: 'text', left: 'center', top: '45%', style: { text: String(total), fontSize: 24, fontWeight: 700, fill: ink } }
|
|
||||||
] : []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([() => trendData.value, () => statusSlices.value, () => themeStore.current], async () => {
|
|
||||||
await nextTick()
|
|
||||||
renderTrend()
|
|
||||||
renderStatus()
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.resize() })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -315,13 +241,13 @@ window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.res
|
|||||||
<div class="rr-card-header">
|
<div class="rr-card-header">
|
||||||
<h3 class="rr-card-title">订单趋势(近 7 天)</h3>
|
<h3 class="rr-card-title">订单趋势(近 7 天)</h3>
|
||||||
</div>
|
</div>
|
||||||
<div ref="trendChartEl" class="rr-chart"></div>
|
<TrendChart :dates="trendData.dates" :counts="trendData.counts" :amounts="trendData.amounts" />
|
||||||
</div>
|
</div>
|
||||||
<div class="rr-card rr-chart-card">
|
<div class="rr-card rr-chart-card">
|
||||||
<div class="rr-card-header">
|
<div class="rr-card-header">
|
||||||
<h3 class="rr-card-title">订单状态分布</h3>
|
<h3 class="rr-card-title">订单状态分布</h3>
|
||||||
</div>
|
</div>
|
||||||
<div ref="statusChartEl" class="rr-chart"></div>
|
<StatusPie :slices="statusSlices" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -338,7 +264,7 @@ window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.res
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-dashboard { max-width: 1400px; }
|
.rr-dashboard { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 20px; }
|
.rr-page-header { margin-bottom: 20px; }
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; }
|
.rr-page-title { font-size: 20px; font-weight: 700; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; }
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
||||||
.rr-api-badge { margin-left: 8px; }
|
.rr-api-badge { margin-left: 8px; }
|
||||||
.rr-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
|
.rr-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
|
||||||
|
|||||||
94
src/modules/statistics/charts/EchartsStatusPie.vue
Normal file
94
src/modules/statistics/charts/EchartsStatusPie.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 统计页 - 订单状态分布环形图 (2026-08-18 从 StatisticsView 抽出)
|
||||||
|
* echarts 动态 import → 独立 chunk 懒加载
|
||||||
|
*/
|
||||||
|
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
|
defineOptions({ name: 'EchartsStatusPie' })
|
||||||
|
|
||||||
|
const props = defineProps<{ slices: { name: string; value: number; color?: string }[] }>()
|
||||||
|
|
||||||
|
const el = ref<HTMLDivElement | null>(null)
|
||||||
|
let chart: any = null
|
||||||
|
let echartsMod: any = null
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
const total = computed(() => props.slices.reduce((a, b) => a + b.value, 0))
|
||||||
|
|
||||||
|
async function ensureEcharts() {
|
||||||
|
if (echartsMod) return echartsMod
|
||||||
|
const core = await import('echarts/core')
|
||||||
|
const { PieChart } = await import('echarts/charts')
|
||||||
|
const { TooltipComponent, LegendComponent } = await import('echarts/components')
|
||||||
|
const { CanvasRenderer } = await import('echarts/renderers')
|
||||||
|
core.use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||||
|
echartsMod = core
|
||||||
|
return core
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssVar(name: string, fb: string): string {
|
||||||
|
return getComputedStyle(document.body).getPropertyValue(name).trim() || fb
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChartColors(): string[] {
|
||||||
|
return ['--chart-1', '--chart-2', '--chart-3', '--chart-4', '--red'].map(v => cssVar(v, '')) || ['#2563EB', '#7C3AED', '#10B981', '#D97706', '#DC2626']
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!el.value || !chart) return
|
||||||
|
const ink = cssVar('--ink-3', '#64748B')
|
||||||
|
const border = cssVar('--border', '#E2E8F0')
|
||||||
|
const surface = cssVar('--surface', '#FFFFFF')
|
||||||
|
const colors = getChartColors()
|
||||||
|
chart.setOption({
|
||||||
|
tooltip: { trigger: 'item', backgroundColor: surface, borderColor: border, textStyle: { color: ink } },
|
||||||
|
legend: { bottom: 0, textStyle: { color: ink } },
|
||||||
|
series: [{
|
||||||
|
type: 'pie', radius: ['58%', '80%'], center: ['50%', '45%'],
|
||||||
|
avoidLabelOverlap: true,
|
||||||
|
label: { show: false }, labelLine: { show: false },
|
||||||
|
data: props.slices.map((s, i) => ({ name: s.name, value: s.value, itemStyle: { color: colors[i % colors.length] } })),
|
||||||
|
emphasis: { label: { show: true, fontSize: 14, fontWeight: 600, color: ink } }
|
||||||
|
}],
|
||||||
|
graphic: total.value > 0 ? [
|
||||||
|
{ type: 'text', left: 'center', top: '38%', style: { text: '总计', fontSize: 10, fill: ink, opacity: 0.5 } },
|
||||||
|
{ type: 'text', left: 'center', top: '45%', style: { text: String(total.value), fontSize: 24, fontWeight: 700, fill: ink } }
|
||||||
|
] : []
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize() { chart?.resize() }
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const mod = await ensureEcharts()
|
||||||
|
if (el.value) {
|
||||||
|
chart = mod.init(el.value)
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
} catch { /* 懒加载失败静默 */ }
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.slices, themeStore.current],
|
||||||
|
() => render(),
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
|
chart?.dispose()
|
||||||
|
chart = null
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="el" class="rr-chart"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rr-chart { width: 100%; height: 300px; }
|
||||||
|
</style>
|
||||||
89
src/modules/statistics/charts/EchartsTrendChart.vue
Normal file
89
src/modules/statistics/charts/EchartsTrendChart.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 统计页 - 订单趋势折线图 (2026-08-18 从 StatisticsView 抽出)
|
||||||
|
* echarts 动态 import → 独立 chunk 懒加载 (StatisticsView 主 chunk 减重)
|
||||||
|
* 主题/数据变化自动重绘
|
||||||
|
*/
|
||||||
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
|
defineOptions({ name: 'EchartsTrendChart' })
|
||||||
|
|
||||||
|
const props = defineProps<{ dates: string[]; counts: number[]; amounts: number[] }>()
|
||||||
|
|
||||||
|
const el = ref<HTMLDivElement | null>(null)
|
||||||
|
let chart: any = null
|
||||||
|
let echartsMod: any = null
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
/** 动态加载 echarts (独立 chunk, 不随主 bundle) */
|
||||||
|
async function ensureEcharts() {
|
||||||
|
if (echartsMod) return echartsMod
|
||||||
|
const core = await import('echarts/core')
|
||||||
|
const { LineChart } = await import('echarts/charts')
|
||||||
|
const { TooltipComponent, GridComponent, LegendComponent } = await import('echarts/components')
|
||||||
|
const { CanvasRenderer } = await import('echarts/renderers')
|
||||||
|
core.use([LineChart, TooltipComponent, GridComponent, LegendComponent, CanvasRenderer])
|
||||||
|
echartsMod = core
|
||||||
|
return core
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssVar(name: string, fb: string): string {
|
||||||
|
return getComputedStyle(document.body).getPropertyValue(name).trim() || fb
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!el.value || !chart) return
|
||||||
|
const ink = cssVar('--ink-3', '#64748B')
|
||||||
|
const border = cssVar('--border', '#E2E8F0')
|
||||||
|
const surface = cssVar('--surface', '#FFFFFF')
|
||||||
|
const brand = cssVar('--brand', '#2563EB')
|
||||||
|
const chart2 = cssVar('--chart-2', '#7C3AED')
|
||||||
|
chart.setOption({
|
||||||
|
grid: { left: 50, right: 50, top: 40, bottom: 30 },
|
||||||
|
tooltip: { trigger: 'axis', backgroundColor: surface, borderColor: border, textStyle: { color: ink } },
|
||||||
|
legend: { top: 0, textStyle: { color: ink } },
|
||||||
|
xAxis: { type: 'category', data: props.dates, axisLine: { lineStyle: { color: border } }, axisLabel: { color: ink } },
|
||||||
|
yAxis: [
|
||||||
|
{ type: 'value', name: '订单数', position: 'left', splitLine: { lineStyle: { color: border } }, axisLabel: { color: ink } },
|
||||||
|
{ type: 'value', name: '交易额', position: 'right', splitLine: { show: false }, axisLabel: { color: ink, formatter: (v: number) => v >= 1000 ? `${(v / 1000).toFixed(0)}K` : String(v) } }
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{ name: '订单数', type: 'line', data: props.counts, smooth: true, itemStyle: { color: brand }, lineStyle: { color: brand, width: 2 }, areaStyle: { color: brand, opacity: 0.15 } },
|
||||||
|
{ name: '交易额', type: 'line', yAxisIndex: 1, data: props.amounts, smooth: true, itemStyle: { color: chart2 }, lineStyle: { color: chart2, width: 2 } }
|
||||||
|
]
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize() { chart?.resize() }
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const mod = await ensureEcharts()
|
||||||
|
if (el.value) {
|
||||||
|
chart = mod.init(el.value)
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
} catch { /* echarts 懒加载失败时静默 (图表留空) */ }
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.dates, props.counts, props.amounts, themeStore.current],
|
||||||
|
() => render()
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
|
chart?.dispose()
|
||||||
|
chart = null
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="el" class="rr-chart"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rr-chart { width: 100%; height: 300px; }
|
||||||
|
</style>
|
||||||
@ -168,9 +168,9 @@ onMounted(load)
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="满减配置" min-width="180" show-overflow-tooltip>
|
<el-table-column label="满减配置" min-width="180" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="(row.type === 1 ? 'warning' : 'success') as any" size="small" effect="light">
|
<span class="st-pill" :style="row.type === 1 ? { color: 'var(--st-warn-fg)', background: 'var(--st-warn-bg)' } : { color: 'var(--st-ok-fg)', background: 'var(--st-ok-bg)' }">
|
||||||
{{ fmtConfig(row) }}
|
<span class="st-dot"></span>{{ fmtConfig(row) }}
|
||||||
</el-tag>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="适用商品" min-width="180" show-overflow-tooltip>
|
<el-table-column label="适用商品" min-width="180" show-overflow-tooltip>
|
||||||
@ -191,9 +191,9 @@ onMounted(load)
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="90" align="center">
|
<el-table-column label="状态" width="90" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="(row.onShelf === 1 ? 'success' : 'info') as any" size="small" effect="light">
|
<span class="st-pill" :style="row.onShelf === 1 ? { color: 'var(--st-ok-fg)', background: 'var(--st-ok-bg)' } : { color: 'var(--st-lv4-fg)', background: 'var(--st-lv4-bg)' }">
|
||||||
{{ row.onShelf === 1 ? '启用' : '停用' }}
|
<span class="st-dot"></span>{{ row.onShelf === 1 ? '启用' : '停用' }}
|
||||||
</el-tag>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||||
@ -221,9 +221,9 @@ onMounted(load)
|
|||||||
<el-table-column prop="userName" label="领取人" align="center" />
|
<el-table-column prop="userName" label="领取人" align="center" />
|
||||||
<el-table-column label="状态" align="center">
|
<el-table-column label="状态" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.status === 1" size="small" type="info" effect="light">已使用</el-tag>
|
<span v-if="row.status === 1" class="st-pill" style="color: var(--st-lv4-fg); background: var(--st-lv4-bg)"><span class="st-dot"></span>已使用</span>
|
||||||
<el-tag v-else-if="row.status === 2" size="small" type="success" effect="light">未使用</el-tag>
|
<span v-else-if="row.status === 2" class="st-pill" style="color: var(--st-ok-fg); background: var(--st-ok-bg)"><span class="st-dot"></span>未使用</span>
|
||||||
<el-tag v-else size="small" type="warning" effect="light">未知</el-tag>
|
<span v-else class="st-pill" style="color: var(--st-warn-fg); background: var(--st-warn-bg)"><span class="st-dot"></span>未知</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="领取时间" align="center" width="200" />
|
<el-table-column prop="createTime" label="领取时间" align="center" width="200" />
|
||||||
|
|||||||
@ -60,15 +60,15 @@ interface SkuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 状态映射 (沿用老 vue checkStatus 1=待审核 2=已通过 3=已驳回, onShelf 0=下架 1=在售)
|
// 状态映射 (沿用老 vue checkStatus 1=待审核 2=已通过 3=已驳回, onShelf 0=下架 1=在售)
|
||||||
const CHECK_STATUS_MAP: Record<number, { label: string; type: string }> = {
|
const CHECK_STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
1: { label: '待审核', type: 'warning' },
|
1: { label: '待审核', color: 'var(--st-warn-fg)', bg: 'var(--st-warn-bg)' },
|
||||||
2: { label: '已通过', type: 'success' },
|
2: { label: '已通过', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
3: { label: '已驳回', type: 'danger' }
|
3: { label: '已驳回', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const ONSHELF_STATUS_MAP: Record<number, { label: string; type: string }> = {
|
const ONSHELF_STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
0: { label: '已下架', type: 'info' },
|
0: { label: '已下架', color: 'var(--st-lv4-fg)', bg: 'var(--st-lv4-bg)' },
|
||||||
1: { label: '已上架', type: 'success' }
|
1: { label: '已上架', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_TABS = [
|
const STATUS_TABS = [
|
||||||
@ -395,14 +395,10 @@ onMounted(() => Promise.all([load(), loadStats(), loadCounts()]))
|
|||||||
<span>{{ [row.firstCategoryName, row.secondCategoryName, row.thirdCategoryName].filter(Boolean).join(' / ') || '--' }}</span>
|
<span>{{ [row.firstCategoryName, row.secondCategoryName, row.thirdCategoryName].filter(Boolean).join(' / ') || '--' }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #onShelf="{ row }">
|
<template #onShelf="{ row }">
|
||||||
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">
|
<span class="st-pill" :style="{ color: ONSHELF_STATUS_MAP[row.onShelf]?.color, background: ONSHELF_STATUS_MAP[row.onShelf]?.bg }"><span class="st-dot"></span>{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</span>
|
||||||
{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
<template #checkStatus="{ row }">
|
<template #checkStatus="{ row }">
|
||||||
<el-tag :type="(CHECK_STATUS_MAP[row.checkStatus]?.type as any)" size="small" effect="light">
|
<span class="st-pill" :style="{ color: CHECK_STATUS_MAP[row.checkStatus]?.color, background: CHECK_STATUS_MAP[row.checkStatus]?.bg }"><span class="st-dot"></span>{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</span>
|
||||||
{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<TableActions
|
<TableActions
|
||||||
@ -451,17 +447,13 @@ onMounted(() => Promise.all([load(), loadStats(), loadCounts()]))
|
|||||||
<el-table-column prop="unitName" label="单位" width="80" align="center" />
|
<el-table-column prop="unitName" label="单位" width="80" align="center" />
|
||||||
<el-table-column label="铺货" width="100" align="center">
|
<el-table-column label="铺货" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">
|
<span class="st-pill" :style="{ color: ONSHELF_STATUS_MAP[row.onShelf]?.color, background: ONSHELF_STATUS_MAP[row.onShelf]?.bg }"><span class="st-dot"></span>{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</span>
|
||||||
{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="checkOpinion" label="审核意见" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="checkOpinion" label="审核意见" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column label="状态" width="100" align="center">
|
<el-table-column label="状态" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="(CHECK_STATUS_MAP[row.checkStatus]?.type as any)" size="small" effect="light">
|
<span class="st-pill" :style="{ color: CHECK_STATUS_MAP[row.checkStatus]?.color, background: CHECK_STATUS_MAP[row.checkStatus]?.bg }"><span class="st-dot"></span>{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</span>
|
||||||
{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="申请时间" prop="createDate" width="170" />
|
<el-table-column label="申请时间" prop="createDate" width="170" />
|
||||||
@ -491,14 +483,10 @@ onMounted(() => Promise.all([load(), loadStats(), loadCounts()]))
|
|||||||
{{ [detailData.firstCategoryName, detailData.secondCategoryName, detailData.thirdCategoryName].filter(Boolean).join(' / ') }}
|
{{ [detailData.firstCategoryName, detailData.secondCategoryName, detailData.thirdCategoryName].filter(Boolean).join(' / ') }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="(CHECK_STATUS_MAP[detailData.checkStatus]?.type as any)" size="small">
|
<span class="st-pill" :style="{ color: CHECK_STATUS_MAP[detailData.checkStatus]?.color, background: CHECK_STATUS_MAP[detailData.checkStatus]?.bg }"><span class="st-dot"></span>{{ CHECK_STATUS_MAP[detailData.checkStatus]?.label }}</span>
|
||||||
{{ CHECK_STATUS_MAP[detailData.checkStatus]?.label }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="铺货">
|
<el-descriptions-item label="铺货">
|
||||||
<el-tag :type="(ONSHELF_STATUS_MAP[detailData.onShelf]?.type as any)" size="small">
|
<span class="st-pill" :style="{ color: ONSHELF_STATUS_MAP[detailData.onShelf]?.color, background: ONSHELF_STATUS_MAP[detailData.onShelf]?.bg }"><span class="st-dot"></span>{{ ONSHELF_STATUS_MAP[detailData.onShelf]?.label }}</span>
|
||||||
{{ ONSHELF_STATUS_MAP[detailData.onShelf]?.label }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="审核意见" :span="2">{{ detailData.checkOpinion || '--' }}</el-descriptions-item>
|
<el-descriptions-item label="审核意见" :span="2">{{ detailData.checkOpinion || '--' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|||||||
@ -36,14 +36,14 @@ interface ServiceGood {
|
|||||||
raw: any
|
raw: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHECK_STATUS_MAP: Record<number, { label: string; type: string }> = {
|
const CHECK_STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
1: { label: '待审核', type: 'warning' },
|
1: { label: '待审核', color: 'var(--st-warn-fg)', bg: 'var(--st-warn-bg)' },
|
||||||
2: { label: '已通过', type: 'success' },
|
2: { label: '已通过', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
3: { label: '已驳回', type: 'danger' }
|
3: { label: '已驳回', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
}
|
}
|
||||||
const ONSHELF_STATUS_MAP: Record<number, { label: string; type: string }> = {
|
const ONSHELF_STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
0: { label: '已下架', type: 'info' },
|
0: { label: '已下架', color: 'var(--st-lv4-fg)', bg: 'var(--st-lv4-bg)' },
|
||||||
1: { label: '已上架', type: 'success' }
|
1: { label: '已上架', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' }
|
||||||
}
|
}
|
||||||
const STATUS_TABS = [
|
const STATUS_TABS = [
|
||||||
{ value: '', label: '全部' },
|
{ value: '', label: '全部' },
|
||||||
@ -309,10 +309,10 @@ onMounted(() => Promise.all([load(), loadStats(), loadCounts()]))
|
|||||||
<span class="rr-price">¥ {{ Number(row.price || 0).toLocaleString() }} / {{ row.unit }}</span>
|
<span class="rr-price">¥ {{ Number(row.price || 0).toLocaleString() }} / {{ row.unit }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #onShelf="{ row }">
|
<template #onShelf="{ row }">
|
||||||
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</el-tag>
|
<span class="st-pill" :style="{ color: ONSHELF_STATUS_MAP[row.onShelf]?.color, background: ONSHELF_STATUS_MAP[row.onShelf]?.bg }"><span class="st-dot"></span>{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #checkStatus="{ row }">
|
<template #checkStatus="{ row }">
|
||||||
<el-tag :type="(CHECK_STATUS_MAP[row.checkStatus]?.type as any)" size="small" effect="light">{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</el-tag>
|
<span class="st-pill" :style="{ color: CHECK_STATUS_MAP[row.checkStatus]?.color, background: CHECK_STATUS_MAP[row.checkStatus]?.bg }"><span class="st-dot"></span>{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<TableActions
|
<TableActions
|
||||||
@ -337,10 +337,10 @@ onMounted(() => Promise.all([load(), loadStats(), loadCounts()]))
|
|||||||
<el-descriptions-item label="服务区域">{{ detailData.serviceArea }}</el-descriptions-item>
|
<el-descriptions-item label="服务区域">{{ detailData.serviceArea }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="价格">¥ {{ detailData.price.toLocaleString() }} / {{ detailData.unit }}</el-descriptions-item>
|
<el-descriptions-item label="价格">¥ {{ detailData.price.toLocaleString() }} / {{ detailData.unit }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="(CHECK_STATUS_MAP[detailData.checkStatus]?.type as any)" size="small">{{ CHECK_STATUS_MAP[detailData.checkStatus]?.label }}</el-tag>
|
<span class="st-pill" :style="{ color: CHECK_STATUS_MAP[detailData.checkStatus]?.color, background: CHECK_STATUS_MAP[detailData.checkStatus]?.bg }"><span class="st-dot"></span>{{ CHECK_STATUS_MAP[detailData.checkStatus]?.label }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="铺货">
|
<el-descriptions-item label="铺货">
|
||||||
<el-tag :type="(ONSHELF_STATUS_MAP[detailData.onShelf]?.type as any)" size="small">{{ ONSHELF_STATUS_MAP[detailData.onShelf]?.label }}</el-tag>
|
<span class="st-pill" :style="{ color: ONSHELF_STATUS_MAP[detailData.onShelf]?.color, background: ONSHELF_STATUS_MAP[detailData.onShelf]?.bg }"><span class="st-dot"></span>{{ ONSHELF_STATUS_MAP[detailData.onShelf]?.label }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="审核意见" :span="2">{{ detailData.checkOpinion || '--' }}</el-descriptions-item>
|
<el-descriptions-item label="审核意见" :span="2">{{ detailData.checkOpinion || '--' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|||||||
@ -226,7 +226,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
||||||
|
|||||||
@ -229,7 +229,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||||
|
|||||||
@ -213,7 +213,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||||
|
|||||||
@ -137,7 +137,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
||||||
|
|||||||
@ -275,7 +275,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
||||||
|
|||||||
@ -260,7 +260,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||||
|
|||||||
@ -221,7 +221,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||||
|
|||||||
@ -254,7 +254,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||||
|
|||||||
@ -189,7 +189,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||||
|
|||||||
@ -193,7 +193,7 @@ onMounted(load)
|
|||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
|
||||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
.rr-page-actions { display: flex; gap: 8px; }
|
||||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||||
|
|||||||
@ -1,30 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 用户管理 - 套老 vue user.vue 结构 + v1.8 设计规范
|
* F006 用户管理 - 2026-08-18 按企业列表 UI 标准升级
|
||||||
*
|
*
|
||||||
* 后端端点 (SysUserController):
|
* 后端端点 (SysUserController):
|
||||||
* GET /sys/user/page 分页列表
|
* GET /sys/user/page 分页列表 (支持 username/userType 过滤)
|
||||||
* GET /sys/user/{id} 详情
|
|
||||||
* POST /sys/user 新增
|
|
||||||
* PUT /sys/user 修改
|
|
||||||
* PUT /sys/user/updateStatus 启用/停用
|
* PUT /sys/user/updateStatus 启用/停用
|
||||||
* PUT /sys/user/password 修改密码(自己)
|
|
||||||
* PUT /sys/user/resetPassword 重置密码(管理员)
|
* PUT /sys/user/resetPassword 重置密码(管理员)
|
||||||
* DELETE /sys/user 删除
|
* DELETE /sys/user 删除 (支持批量)
|
||||||
*
|
*
|
||||||
* 字段(来自 SysUserDTO):
|
* 字段(来自 SysUserDTO):
|
||||||
* id / username / password(WRITE_ONLY) / realName / headUrl
|
* id / username / realName / headUrl / gender / email / mobile / deptId
|
||||||
* gender(0=男/1=女/2=保密) / email / mobile / deptId
|
* superAdmin(0=否/1=是) / status(0=停用/1=正常) / employeeId(工号)
|
||||||
* superAdmin(0=否/1=是) / status(0=停用/1=正常) / remark
|
* userType(1=试用/2=正式/3=外包/99=AI公司) / createDate / deptName / roleNames
|
||||||
* identifier / employeeId(工号) / userType(1=试用/2=正式/3=外包/99=AI公司)
|
|
||||||
* joinDate / invitationUrl / invitationCode / xcxQrCode / operationId
|
|
||||||
* createDate / deptName / roleNames / roleIdList / postIdList
|
|
||||||
*
|
*
|
||||||
* 设计规范:§16 列表页 + §17.7 紧凑弹窗 + §17.8 主题色弹窗 + §17.9 列优化
|
* UI 标准对齐企业列表: 头像渐变单元 / 两行信息列 / 圆点状态胶囊(全局 --st-* 变量暗色自适应) /
|
||||||
|
* 搜索栏补用户类型下拉 / selection 批量删除 / 清理老式死样式
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet, httpPost, httpPut, httpDelete } from '@/utils/request'
|
import { User, Phone, Clock } from '@element-plus/icons-vue'
|
||||||
|
import { httpGet, httpPut, httpDelete } from '@/utils/request'
|
||||||
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,42 +30,38 @@ interface SysUser {
|
|||||||
username: string
|
username: string
|
||||||
realName: string
|
realName: string
|
||||||
headUrl?: string | null
|
headUrl?: string | null
|
||||||
gender?: number | null
|
|
||||||
email?: string | null
|
email?: string | null
|
||||||
mobile?: string | null
|
mobile?: string | null
|
||||||
deptId?: string | null
|
|
||||||
deptName?: string | null
|
deptName?: string | null
|
||||||
superAdmin?: number
|
superAdmin?: number
|
||||||
status: 0 | 1
|
status: 0 | 1
|
||||||
remark?: string | null
|
|
||||||
identifier?: string | null
|
|
||||||
employeeId?: string | null
|
employeeId?: string | null
|
||||||
userType?: number
|
userType?: number
|
||||||
joinDate?: string | number | null
|
|
||||||
createDate: string | number
|
createDate: string | number
|
||||||
roleNames?: string | null
|
roleNames?: string | null
|
||||||
roleIdList?: number[] | null
|
|
||||||
postIdList?: number[] | null
|
|
||||||
raw: any
|
raw: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
const STATUS_META: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
1: { label: '正常', type: 'success' },
|
1: { label: '正常', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
0: { label: '停用', type: 'danger' }
|
0: { label: '停用', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_TYPE_MAP: Record<number, { label: string; type: string }> = {
|
const USER_TYPE_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
1: { label: '试用期', type: 'info' },
|
1: { label: '试用期', color: 'var(--st-lv4-fg)', bg: 'var(--st-lv4-bg)' },
|
||||||
2: { label: '正式', type: 'success' },
|
2: { label: '正式', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
3: { label: '外包', type: 'warning' },
|
3: { label: '外包', color: 'var(--st-warn-fg)', bg: 'var(--st-warn-bg)' },
|
||||||
99: { label: 'AI 公司', type: 'info' }
|
99: { label: 'AI 公司', color: 'var(--st-lv2-fg)', bg: 'var(--st-lv2-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const GENDER_MAP: Record<number, string> = {
|
// 头像品牌渐变色组 (无 headUrl 时交替使用, 与企业列表同源风格)
|
||||||
0: '男',
|
const AVATAR_GRADIENTS = [
|
||||||
1: '女',
|
'linear-gradient(135deg, #0b5394, #6fb1e8)',
|
||||||
2: '保密'
|
'linear-gradient(135deg, #2563eb, #93c5fd)',
|
||||||
}
|
'linear-gradient(135deg, #0e7490, #67e8f9)',
|
||||||
|
'linear-gradient(135deg, #7c3aed, #c4b5fd)',
|
||||||
|
'linear-gradient(135deg, #be123c, #fda4af)'
|
||||||
|
]
|
||||||
|
|
||||||
const list = ref<SysUser[]>([])
|
const list = ref<SysUser[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@ -78,6 +69,8 @@ const loading = ref(false)
|
|||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const limit = ref(10)
|
const limit = ref(10)
|
||||||
const loadError = ref<string | null>(null)
|
const loadError = ref<string | null>(null)
|
||||||
|
const selectedIds = ref<string[]>([])
|
||||||
|
const selectedCount = ref(0)
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
@ -88,6 +81,14 @@ const resetPwdVisible = ref(false)
|
|||||||
const resetPwdTarget = ref<SysUser | null>(null)
|
const resetPwdTarget = ref<SysUser | null>(null)
|
||||||
const newPassword = ref('')
|
const newPassword = ref('')
|
||||||
|
|
||||||
|
function avatarStyle(row: SysUser, index: number) {
|
||||||
|
return { background: AVATAR_GRADIENTS[index % AVATAR_GRADIENTS.length] }
|
||||||
|
}
|
||||||
|
function initial(row: SysUser): string {
|
||||||
|
const name = row.realName && row.realName !== '--' ? row.realName : row.username
|
||||||
|
return (name || '用').trim().charAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
function fmtDate(ts?: string | number | null) {
|
function fmtDate(ts?: string | number | null) {
|
||||||
if (!ts) return '--'
|
if (!ts) return '--'
|
||||||
const n = typeof ts === 'string' ? Date.parse(ts) : ts
|
const n = typeof ts === 'string' ? Date.parse(ts) : ts
|
||||||
@ -113,22 +114,14 @@ async function load() {
|
|||||||
username: it.username || '--',
|
username: it.username || '--',
|
||||||
realName: it.realName || '--',
|
realName: it.realName || '--',
|
||||||
headUrl: it.headUrl,
|
headUrl: it.headUrl,
|
||||||
gender: it.gender,
|
|
||||||
email: it.email,
|
|
||||||
mobile: it.mobile,
|
mobile: it.mobile,
|
||||||
deptId: it.deptId ? String(it.deptId) : null,
|
|
||||||
deptName: it.deptName || '--',
|
deptName: it.deptName || '--',
|
||||||
superAdmin: it.superAdmin ?? 0,
|
superAdmin: it.superAdmin ?? 0,
|
||||||
status: (it.status ?? 0) as 0 | 1,
|
status: (it.status ?? 0) as 0 | 1,
|
||||||
remark: it.remark,
|
|
||||||
identifier: it.identifier,
|
|
||||||
employeeId: it.employeeId || '--',
|
employeeId: it.employeeId || '--',
|
||||||
userType: it.userType,
|
userType: it.userType,
|
||||||
joinDate: it.joinDate,
|
|
||||||
createDate: it.createDate,
|
createDate: it.createDate,
|
||||||
roleNames: it.roleNames,
|
roleNames: it.roleNames,
|
||||||
roleIdList: it.roleIdList,
|
|
||||||
postIdList: it.postIdList,
|
|
||||||
raw: it
|
raw: it
|
||||||
}))
|
}))
|
||||||
total.value = data.total || 0
|
total.value = data.total || 0
|
||||||
@ -151,6 +144,7 @@ function onReset() {
|
|||||||
}
|
}
|
||||||
function onPageChange(p: number) { page.value = p; load() }
|
function onPageChange(p: number) { page.value = p; load() }
|
||||||
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||||
|
function onSelectionChange(rows: any[]) { selectedIds.value = rows.map((r: any) => r.id); selectedCount.value = rows.length }
|
||||||
|
|
||||||
async function toggleStatus(row: SysUser) {
|
async function toggleStatus(row: SysUser) {
|
||||||
const newStatus = row.status === 1 ? 0 : 1
|
const newStatus = row.status === 1 ? 0 : 1
|
||||||
@ -165,10 +159,7 @@ async function toggleStatus(row: SysUser) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await httpPut('/sys/user/updateStatus', {
|
await httpPut('/sys/user/updateStatus', { id: Number(row.id), status: newStatus })
|
||||||
id: Number(row.id),
|
|
||||||
status: newStatus
|
|
||||||
})
|
|
||||||
ElMessage.success(`${action}成功`)
|
ElMessage.success(`${action}成功`)
|
||||||
load()
|
load()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -184,14 +175,8 @@ function resetPwdHandle(row: SysUser) {
|
|||||||
|
|
||||||
async function resetPwdConfirm() {
|
async function resetPwdConfirm() {
|
||||||
if (!resetPwdTarget.value) return
|
if (!resetPwdTarget.value) return
|
||||||
if (!newPassword.value.trim()) {
|
if (!newPassword.value.trim()) { ElMessage.warning('请输入新密码'); return }
|
||||||
ElMessage.warning('请输入新密码')
|
if (newPassword.value.length < 6) { ElMessage.warning('密码至少 6 位'); return }
|
||||||
return
|
|
||||||
}
|
|
||||||
if (newPassword.value.length < 6) {
|
|
||||||
ElMessage.warning('密码至少 6 位')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await httpPut('/sys/user/resetPassword', {
|
await httpPut('/sys/user/resetPassword', {
|
||||||
id: Number(resetPwdTarget.value.id),
|
id: Number(resetPwdTarget.value.id),
|
||||||
@ -208,28 +193,35 @@ async function resetPwdConfirm() {
|
|||||||
|
|
||||||
async function deleteHandle(row: SysUser) {
|
async function deleteHandle(row: SysUser) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(`确认删除用户 ${row.username} 吗?删除后无法恢复。`, '删除确认',
|
||||||
`确认删除用户 ${row.username} 吗?删除后无法恢复。`,
|
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
|
||||||
'删除确认',
|
} catch { return }
|
||||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await httpDelete(`/sys/user/${row.id}`)
|
await httpDelete(`/sys/user/${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 || '删除失败')
|
}
|
||||||
|
|
||||||
|
async function batchDelete() {
|
||||||
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择用户'); return }
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除已选 ${selectedIds.value.length} 个用户?删除后无法恢复。`, '批量删除确认',
|
||||||
|
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
|
||||||
|
} catch { return }
|
||||||
|
let ok = 0
|
||||||
|
for (const id of selectedIds.value) {
|
||||||
|
try { await httpDelete(`/sys/user/${id}`); ok++ } catch {/* 单条失败跳过 */}
|
||||||
}
|
}
|
||||||
|
ElMessage.success(`已删除 ${ok} 个用户`)
|
||||||
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => { load() })
|
onMounted(() => { load() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-user">
|
<div class="rr-page">
|
||||||
<PageHeader title="后台用户" :sub="`共 ${total} 个用户`">
|
<PageHeader title="后台用户" :sub="`共 ${total} 个用户`">
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
@ -237,68 +229,116 @@ onMounted(() => { load() })
|
|||||||
<SearchBar
|
<SearchBar
|
||||||
v-model:filters="filters"
|
v-model:filters="filters"
|
||||||
:fields='[
|
:fields='[
|
||||||
{ key: "username", label: "用户名", type: "text", placeholder: "用户名" }
|
{ key: "username", label: "用户名", type: "text", placeholder: "用户名" },
|
||||||
|
{ key: "userType", label: "用户类型", type: "select", placeholder: "全部类型",
|
||||||
|
options: [
|
||||||
|
{ value: 1, label: "试用期" },
|
||||||
|
{ value: 2, label: "正式" },
|
||||||
|
{ value: 3, label: "外包" },
|
||||||
|
{ value: 99, label: "AI 公司" }
|
||||||
|
] }
|
||||||
]'
|
]'
|
||||||
@search="onSearch"
|
@search="onSearch"
|
||||||
@reset="onReset"
|
@reset="onReset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
|
<!-- 批量操作栏 -->
|
||||||
|
<div class="batch-bar" :class="{ 'has-selection': selectedCount > 0 }">
|
||||||
|
<span v-if="selectedCount > 0" class="batch-count">
|
||||||
|
已选 <strong>{{ selectedCount }}</strong> 个用户
|
||||||
|
</span>
|
||||||
|
<span v-else class="batch-hint">勾选用户后可批量删除</span>
|
||||||
|
<span class="batch-actions">
|
||||||
|
<el-button size="small" type="danger" plain :disabled="!selectedCount" @click="batchDelete">批量删除</el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
:columns="[
|
:columns="[
|
||||||
{ prop: 'username', label: '用户名', width: 200, slot: 'username' },
|
{ type: 'selection', width: 48, label: '' },
|
||||||
{ prop: 'realName', label: '姓名', width: 140 },
|
{ label: '用户', minWidth: 220, slot: 'user' },
|
||||||
{ prop: 'employeeId', label: '工号', width: 120 },
|
{ prop: 'mobile', label: '手机号', width: 130, slot: 'mobile' },
|
||||||
{ prop: 'mobile', label: '手机号', width: 130 },
|
{ label: '部门 / 角色', minWidth: 180, slot: 'deptRole' },
|
||||||
{ prop: 'userType', label: '类型', width: 100, slot: 'userType' },
|
{ prop: 'userType', label: '类型', width: 100, slot: 'userType', align: 'center' },
|
||||||
{ prop: 'status', label: '状态', width: 80, slot: 'status' },
|
{ prop: 'status', label: '状态', width: 90, slot: 'status', align: 'center' },
|
||||||
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
{ label: '创建时间', width: 165, slot: 'createDate' },
|
||||||
{ label: '操作', width: 140, slot: 'action' }
|
{ label: '操作', width: 170, slot: 'action', fixed: 'right', align: 'center' }
|
||||||
]"
|
]"
|
||||||
:data="list" :borderless="true"
|
:data="list"
|
||||||
|
:borderless="true"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
:show-pagination="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:error="loadError"
|
:error="loadError"
|
||||||
empty-text="暂无用户"
|
empty-text="暂无用户"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
@limit-change="onLimitChange"
|
@limit-change="onLimitChange"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<template #username="{ row }">
|
<!-- 用户单元: 头像 + 用户名 + 姓名/工号 -->
|
||||||
<code class="rr-username-code">{{ row.username }}</code>
|
<template #user="{ row, index }">
|
||||||
|
<div class="user-cell">
|
||||||
|
<span class="user-avatar" :style="avatarStyle(row, index)">
|
||||||
|
<el-image v-if="row.headUrl" :src="row.headUrl" fit="cover" class="user-avatar-img" />
|
||||||
|
<template v-else>{{ initial(row) }}</template>
|
||||||
|
</span>
|
||||||
|
<span class="user-text">
|
||||||
|
<span class="user-name">
|
||||||
|
{{ row.username }}
|
||||||
|
<el-tag v-if="row.superAdmin === 1" size="small" type="warning" effect="light" class="super-tag">超管</el-tag>
|
||||||
|
</span>
|
||||||
|
<span class="user-sub">{{ row.realName }}{{ row.employeeId && row.employeeId !== '--' ? ` · ${row.employeeId}` : '' }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 手机号 -->
|
||||||
|
<template #mobile="{ row }">
|
||||||
|
<span class="phone-cell"><el-icon><Phone /></el-icon>{{ row.mobile || '--' }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 部门/角色 -->
|
||||||
|
<template #deptRole="{ row }">
|
||||||
|
<div class="two-line">
|
||||||
|
<span class="line-main">{{ row.deptName }}</span>
|
||||||
|
<span class="line-sub">{{ row.roleNames || '未分配角色' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 类型 -->
|
||||||
<template #userType="{ row }">
|
<template #userType="{ row }">
|
||||||
<el-tag
|
<span v-if="row.userType != null && USER_TYPE_MAP[row.userType]" class="chip"
|
||||||
v-if="row.userType != null && USER_TYPE_MAP[row.userType]"
|
:style="{ color: USER_TYPE_MAP[row.userType].color, background: USER_TYPE_MAP[row.userType].bg }">
|
||||||
:type="USER_TYPE_MAP[row.userType].type as any"
|
{{ USER_TYPE_MAP[row.userType].label }}
|
||||||
size="small"
|
</span>
|
||||||
effect="light"
|
|
||||||
>{{ USER_TYPE_MAP[row.userType].label }}</el-tag>
|
|
||||||
<span v-else class="rr-muted">--</span>
|
<span v-else class="rr-muted">--</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 状态: 圆点胶囊 -->
|
||||||
<template #status="{ row }">
|
<template #status="{ row }">
|
||||||
<el-tag
|
<span class="status-tag"
|
||||||
:type="STATUS_MAP[row.status].type as any"
|
:style="{ color: STATUS_META[row.status].color, background: STATUS_META[row.status].bg }">
|
||||||
size="small"
|
<span class="status-dot"></span>{{ STATUS_META[row.status].label }}
|
||||||
effect="light"
|
</span>
|
||||||
>{{ STATUS_MAP[row.status].label }}</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 创建时间 -->
|
||||||
<template #createDate="{ row }">
|
<template #createDate="{ row }">
|
||||||
<span class="rr-time">{{ fmtDate(row.createDate) }}</span>
|
<span class="date-cell"><el-icon><Clock /></el-icon>{{ fmtDate(row.createDate) }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<TableActions
|
<TableActions
|
||||||
:row="row"
|
:row="row"
|
||||||
:actions="[
|
:actions="[
|
||||||
{ label: (r: any) => r.status === 1 ? '停用' : '启用', type: (r: any) => r.status === 1 ? 'warning' : 'success', onClick: toggleStatus },
|
{ label: (r: any) => r.status === 1 ? '停用' : '启用', type: (r: any) => r.status === 1 ? 'warning' : 'success', onClick: toggleStatus },
|
||||||
{ label: '重置密码', type: 'primary', onClick: resetPwdHandle },
|
{ label: '重置密码', type: 'info', onClick: resetPwdHandle },
|
||||||
{ label: '删除', type: 'danger', onClick: deleteHandle }
|
{ label: '删除', type: 'danger', onClick: deleteHandle }
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
@ -311,11 +351,10 @@ onMounted(() => { load() })
|
|||||||
v-model="resetPwdVisible"
|
v-model="resetPwdVisible"
|
||||||
title="重置密码"
|
title="重置密码"
|
||||||
width="480"
|
width="480"
|
||||||
:style="{ borderRadius: '12px', overflow: 'hidden' }"
|
|
||||||
align-center
|
align-center
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<p v-if="resetPwdTarget" style="margin-bottom: 16px;">
|
<p v-if="resetPwdTarget" style="margin: 0 0 16px; color: var(--ink-2)">
|
||||||
用户: <code class="rr-username-code">{{ resetPwdTarget.username }}</code>
|
用户: <code class="rr-username-code">{{ resetPwdTarget.username }}</code>
|
||||||
({{ resetPwdTarget.realName }})
|
({{ resetPwdTarget.realName }})
|
||||||
</p>
|
</p>
|
||||||
@ -330,9 +369,8 @@ onMounted(() => { load() })
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-alert type="warning" :closable="false" style="margin-top: 8px;">
|
<el-alert type="warning" :closable="false" style="margin-top: 8px">
|
||||||
<p>重置后请通过安全渠道告知用户</p>
|
<p style="margin: 0">重置后请通过安全渠道告知用户,下次登录需使用新密码</p>
|
||||||
<p>用户下次登录需使用新密码</p>
|
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="resetPwdVisible = false">取消</el-button>
|
<el-button @click="resetPwdVisible = false">取消</el-button>
|
||||||
@ -342,78 +380,64 @@ onMounted(() => { load() })
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.rr-sys-user {
|
.rr-page { padding: 16px; }
|
||||||
padding: 16px;
|
|
||||||
|
/* ===== 批量操作栏 (对齐企业列表) ===== */
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
/* ===== 用户单元 ===== */
|
||||||
|
.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 {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
font-size: 14px; font-weight: 600; color: var(--ink);
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.super-tag { margin-left: 2px; }
|
||||||
|
.user-sub {
|
||||||
|
font-size: 12px; color: var(--ink-3);
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rr-page-header {
|
/* ===== 单元格通用 ===== */
|
||||||
display: flex;
|
.two-line { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||||||
align-items: center;
|
.line-main { font-size: 13px; color: var(--ink); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
justify-content: space-between;
|
.line-sub { font-size: 12px; color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
margin-bottom: 14px;
|
.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; }
|
||||||
|
|
||||||
.rr-page-titles {
|
/* ===== 类型/状态胶囊 (对齐企业列表, 使用全局 --st-* 变量暗色自适应) ===== */
|
||||||
display: flex;
|
.chip {
|
||||||
flex-direction: column;
|
display: inline-flex; align-items: center; width: fit-content;
|
||||||
gap: 2px;
|
font-size: 12px; font-weight: 500; padding: 2px 12px; border-radius: 20px; white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.status-tag {
|
||||||
.rr-page-title {
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
margin: 0;
|
font-size: 12px; font-weight: 500; padding: 3px 12px; border-radius: 20px; white-space: nowrap;
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-sub {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-filters-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-username-input { width: 240px; }
|
|
||||||
.rr-type-select { width: 140px; }
|
|
||||||
|
|
||||||
.rr-list-card {
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
}
|
||||||
|
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||||
|
|
||||||
.rr-username-code {
|
.rr-username-code {
|
||||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px; padding: 2px 6px;
|
||||||
padding: 2px 6px;
|
background: var(--fill-color-light, var(--surface-2));
|
||||||
background: var(--el-fill-color-light);
|
border-radius: 4px; color: var(--el-color-primary);
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-time {
|
|
||||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-muted {
|
|
||||||
color: var(--el-text-color-placeholder);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
</style>
|
</style>
|
||||||
@ -1,18 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* W18.2 企业列表 — 按项目 UI 规范 (PageHeader/SearchBar/DataTable) + 老 vue 全功能
|
* W18.2 + 2026-08-18 企业列表 — 专业 UI 升级版 (按项目 UI 规范 + 老 vue 全功能)
|
||||||
*
|
*
|
||||||
* 后端端点: ApiEnterpriseController @ /user/enterprise
|
* 后端端点: ApiEnterpriseController @ /user/enterprise
|
||||||
* GET /user/enterprise/page 分页 (EnterpriseDTO 全字段, 支持 enterpriseName/industry/supplierLevel/salesperson/supplierLabel/status/homeShow 过滤)
|
* GET /user/enterprise/page 分页 (EnterpriseDTO 全字段, 支持 enterpriseName/industry/supplierLevel/salesperson/supplierLabel/status/homeShow 过滤)
|
||||||
|
* GET /user/enterprise/checkStatus?id=&status=&failReason= 审核(2通过/3驳回, 支持逗号批量)
|
||||||
|
* POST /user/enterprise/enabled?id= 禁用/启用切换 (后端 @RequestParam 从 query 读)
|
||||||
|
* POST /user/enterprise/showEnterprise?id= 显示/隐藏切换 (query)
|
||||||
|
* GET /user/enterprise/homeShow?id=&homeShow= 首页推荐切换 (query)
|
||||||
*
|
*
|
||||||
* EnterpriseDTO 真实字段 (来源: devbox 编译反编译确认, 2026-07-15):
|
* EnterpriseDTO 真实字段 (来源: devbox 编译反编译确认, 2026-07-15):
|
||||||
* id/enterpriseName/enterpriseLogo/administrator/administratorMobile/industry/supplierLevel
|
* id/enterpriseName/enterpriseLogo/administrator/administratorMobile/industry/supplierLevel
|
||||||
* supplierLabel/salesperson/salespersonMobile/status/createDate/enabled/showFlag/homeShow
|
* supplierLabel/salesperson/salespersonMobile/status/createDate/enabled/showFlag/homeShow
|
||||||
* linkMan/linkMobile/legalMan/email/enterpriseAddress/mainProducts/registerTime/orgNo/failReason
|
* linkMan/linkMobile/legalMan/email/enterpriseAddress/mainProducts/registerTime/orgNo/failReason
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { User, Clock, Upload, Download, MoreFilled } from '@element-plus/icons-vue'
|
||||||
import { httpGet, httpPost, httpDelete } from '@/utils/request'
|
import { httpGet, httpPost, httpDelete } from '@/utils/request'
|
||||||
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'
|
||||||
@ -58,8 +63,17 @@ const loading = ref(false)
|
|||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const limit = ref(10)
|
const limit = ref(10)
|
||||||
const selectedIds = ref<string[]>([])
|
const selectedIds = ref<string[]>([])
|
||||||
const detailVisible = ref(false)
|
const selectedRows = ref<Enterprise[]>([])
|
||||||
const detailRow = ref<Enterprise | null>(null)
|
|
||||||
|
// ========== 单行审核 ==========
|
||||||
|
const auditVisible = ref(false)
|
||||||
|
const auditBusy = ref(false)
|
||||||
|
const auditForm = reactive({ id: '', result: 2, failReason: '' })
|
||||||
|
|
||||||
|
// ========== 批量审核 ==========
|
||||||
|
const batchAuditVisible = ref(false)
|
||||||
|
const batchAuditBusy = ref(false)
|
||||||
|
const batchAuditForm = reactive({ result: 2, failReason: '' })
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
enterpriseName: '',
|
enterpriseName: '',
|
||||||
@ -72,19 +86,33 @@ const filters = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// status 映射 (老 vue: 1=待审核,2=已认证,3=已驳回)
|
// status 映射 (老 vue: 1=待审核,2=已认证,3=已驳回)
|
||||||
const STATUS_MAP: Record<string, string> = {
|
// 状态/等级色引用全局 token (styles/global-ui.scss), 主题切换自动适配暗色
|
||||||
'1': '待审核', '2': '已认证', '3': '已驳回'
|
const STATUS_META: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
}
|
'1': { label: '待审核', color: 'var(--st-warn-fg)', bg: 'var(--st-warn-bg)' },
|
||||||
const STATUS_CLASS: Record<string, string> = {
|
'2': { label: '已认证', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
'1': 'rr-tag-warn', '2': 'rr-tag-ok', '3': 'rr-tag-err'
|
'3': { label: '已驳回', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// supplierLevel 映射 (老 vue: 1=优质,2=钻石,3=金牌,4=银牌,5=铜牌)
|
// supplierLevel 映射 (老 vue: 1=优质,2=钻石,3=金牌,4=银牌,5=铜牌)
|
||||||
const LEVEL_MAP: Record<number, string> = {
|
const LEVEL_META: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
1: '优质供应商', 2: '钻石供应商', 3: '金牌供应商',
|
1: { label: '优质供应商', color: 'var(--st-lv1-fg)', bg: 'var(--st-lv1-bg)' },
|
||||||
4: '银牌供应商', 5: '铜牌供应商'
|
2: { label: '钻石供应商', color: 'var(--st-lv2-fg)', bg: 'var(--st-lv2-bg)' },
|
||||||
|
3: { label: '金牌供应商', color: 'var(--st-lv3-fg)', bg: 'var(--st-lv3-bg)' },
|
||||||
|
4: { label: '银牌供应商', color: 'var(--st-lv4-fg)', bg: 'var(--st-lv4-bg)' },
|
||||||
|
5: { label: '铜牌供应商', color: 'var(--st-lv5-fg)', bg: 'var(--st-lv5-bg)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 头像品牌渐变色组 (企业无 logo 时交替使用)
|
||||||
|
const AVATAR_GRADIENTS = [
|
||||||
|
'linear-gradient(135deg, #0b5394, #6fb1e8)',
|
||||||
|
'linear-gradient(135deg, #2563eb, #93c5fd)',
|
||||||
|
'linear-gradient(135deg, #0e7490, #67e8f9)',
|
||||||
|
'linear-gradient(135deg, #7c3aed, #c4b5fd)',
|
||||||
|
'linear-gradient(135deg, #be123c, #fda4af)'
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedCount = computed(() => selectedIds.value.length)
|
||||||
|
|
||||||
function fmtDate(ts?: string | null) {
|
function fmtDate(ts?: string | null) {
|
||||||
if (!ts) return '--'
|
if (!ts) return '--'
|
||||||
const d = new Date(ts)
|
const d = new Date(ts)
|
||||||
@ -100,6 +128,16 @@ function maskMobile(mobile?: string | null) {
|
|||||||
|
|
||||||
/** 企业名首字 (Avatar 用) */
|
/** 企业名首字 (Avatar 用) */
|
||||||
function initial(name: string) { return name ? name.charAt(0) : '企' }
|
function initial(name: string) { return name ? name.charAt(0) : '企' }
|
||||||
|
/** 头像背景 (有 logo 用图片, 否则渐变) */
|
||||||
|
function avatarStyle(row: Enterprise, index: number) {
|
||||||
|
return { background: AVATAR_GRADIENTS[index % AVATAR_GRADIENTS.length] }
|
||||||
|
}
|
||||||
|
/** 标签前 2 个 + 剩余计数 */
|
||||||
|
function shownTags(row: Enterprise): { tags: string[]; rest: number } {
|
||||||
|
const raw = row.supplierLabel || ''
|
||||||
|
const tags = String(raw).split(/[,,、;;\s]+/).map((s: string) => s.trim()).filter(Boolean)
|
||||||
|
return { tags: tags.slice(0, 2), rest: Math.max(0, tags.length - 2) }
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -158,14 +196,62 @@ function onReset() {
|
|||||||
}
|
}
|
||||||
function onPageChange(p: number) { page.value = p; load() }
|
function onPageChange(p: number) { page.value = p; load() }
|
||||||
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||||
function onSelectionChange(rows: any[]) { selectedIds.value = rows.map((r: any) => r.id) }
|
function onSelectionChange(rows: any[]) { selectedIds.value = rows.map((r: any) => r.id); selectedRows.value = rows }
|
||||||
|
|
||||||
|
// ========== 审核交互 (后端 GET /user/enterprise/checkStatus?id=&status=&failReason=, 支持逗号批量) ==========
|
||||||
|
function onAudit(row: Enterprise) {
|
||||||
|
auditForm.id = row.id
|
||||||
|
auditForm.result = 2
|
||||||
|
auditForm.failReason = ''
|
||||||
|
auditVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAudit() {
|
||||||
|
if (auditForm.result === 3 && !auditForm.failReason.trim()) { ElMessage.warning('请填写驳回原因'); return }
|
||||||
|
auditBusy.value = true
|
||||||
|
try {
|
||||||
|
await httpGet('/user/enterprise/checkStatus', {
|
||||||
|
id: auditForm.id, status: auditForm.result,
|
||||||
|
failReason: auditForm.result === 3 ? auditForm.failReason.trim() : ''
|
||||||
|
})
|
||||||
|
ElMessage.success(auditForm.result === 2 ? '审核通过' : '已驳回')
|
||||||
|
auditVisible.value = false
|
||||||
|
load()
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '审核操作失败') }
|
||||||
|
finally { auditBusy.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openBatchAudit() {
|
||||||
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
|
||||||
|
batchAuditForm.result = 2
|
||||||
|
batchAuditForm.failReason = ''
|
||||||
|
batchAuditVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitBatchAudit() {
|
||||||
|
if (batchAuditForm.result === 3 && !batchAuditForm.failReason.trim()) { ElMessage.warning('请填写驳回原因'); return }
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认对已选 ${selectedIds.value.length} 家企业执行「${batchAuditForm.result === 2 ? '审核通过' : '驳回'}」?` +
|
||||||
|
(batchAuditForm.result === 2 ? '通过后将通过站内消息与短信通知企业。' : ''),
|
||||||
|
'批量审核确认', { type: 'warning' }
|
||||||
|
)
|
||||||
|
} catch { return }
|
||||||
|
batchAuditBusy.value = true
|
||||||
|
try {
|
||||||
|
await httpGet('/user/enterprise/checkStatus', {
|
||||||
|
id: selectedIds.value.join(','), status: batchAuditForm.result,
|
||||||
|
failReason: batchAuditForm.result === 3 ? batchAuditForm.failReason.trim() : ''
|
||||||
|
})
|
||||||
|
ElMessage.success(`已对 ${selectedIds.value.length} 家企业完成审核`)
|
||||||
|
batchAuditVisible.value = false
|
||||||
|
load()
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '批量审核失败') }
|
||||||
|
finally { batchAuditBusy.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 批量操作 ==========
|
// ========== 批量操作 ==========
|
||||||
function importHandle() { ElMessage.info('导入功能待实现') }
|
function importHandle() { ElMessage.info('导入功能待实现') }
|
||||||
function batchAuditHandle() {
|
|
||||||
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
|
|
||||||
ElMessage.info('批量审核功能待实现')
|
|
||||||
}
|
|
||||||
function batchLabelHandle() {
|
function batchLabelHandle() {
|
||||||
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
|
||||||
ElMessage.info('批量设置标签待实现')
|
ElMessage.info('批量设置标签待实现')
|
||||||
@ -188,27 +274,44 @@ async function deleteHandle() {
|
|||||||
function exportSupplierDetail() { ElMessage.info('导出待实现') }
|
function exportSupplierDetail() { ElMessage.info('导出待实现') }
|
||||||
|
|
||||||
// ========== 单行操作 ==========
|
// ========== 单行操作 ==========
|
||||||
function showDetail(row: Enterprise) { detailRow.value = row; detailVisible.value = true }
|
|
||||||
function goDetail(row: Enterprise) { router.push(`/user/enterprise/detail/${row.id}`) }
|
function goDetail(row: Enterprise) { router.push(`/user/enterprise/detail/${row.id}`) }
|
||||||
function changeAdminHandle(_row: Enterprise) { ElMessage.info('更换管理员待实现') }
|
|
||||||
|
// ========== 更换管理员 ==========
|
||||||
|
const adminDialogVisible = ref(false)
|
||||||
|
const adminForm = reactive({ id: '', administrator: '', administratorMobile: '' })
|
||||||
|
function changeAdminHandle(row: Enterprise) {
|
||||||
|
Object.assign(adminForm, { id: row.id, administrator: row.administrator || '', administratorMobile: row.administratorMobile || '' })
|
||||||
|
adminDialogVisible.value = true
|
||||||
|
}
|
||||||
|
async function submitChangeAdmin() {
|
||||||
|
if (!adminForm.administratorMobile?.trim()) { ElMessage.warning('请输入新管理员手机号'); return }
|
||||||
|
if (!adminForm.administrator?.trim()) { ElMessage.warning('请输入新管理员姓名'); return }
|
||||||
|
try {
|
||||||
|
await httpPost('/user/enterprise/updateAdministrator', { ...adminForm })
|
||||||
|
ElMessage.success('更换成功')
|
||||||
|
adminDialogVisible.value = false
|
||||||
|
load()
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || '更换失败') }
|
||||||
|
}
|
||||||
|
|
||||||
async function handleMore(cmd: string, row: Enterprise) {
|
async function handleMore(cmd: string, row: Enterprise) {
|
||||||
try {
|
try {
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 'audit': ElMessage.info('审核待实现'); break
|
case 'audit': onAudit(row); break
|
||||||
case 'label': ElMessage.info('标签待实现'); break
|
case 'label': ElMessage.info('标签待实现'); break
|
||||||
case 'rating': ElMessage.info('评级待实现'); break
|
case 'rating': ElMessage.info('评级待实现'); break
|
||||||
case 'enabled':
|
case 'enabled':
|
||||||
await httpPost('/user/enterprise/enabled', { id: row.id, enabled: row.enabled === 1 ? 0 : 1 })
|
// 后端 @RequestParam 从 query 读, body 不绑定 (2026-08-18 实测修正)
|
||||||
ElMessage.success(row.enabled === 1 ? '已禁用' : '已启用'); load()
|
await httpPost(`/user/enterprise/enabled?id=${row.id}`)
|
||||||
|
ElMessage.success(row.enabled === 1 ? '已启用' : '已禁用'); load()
|
||||||
break
|
break
|
||||||
case 'showFlag':
|
case 'showFlag':
|
||||||
await httpPost('/user/enterprise/showEnterprise', { id: row.id, showFlag: row.showFlag === 1 ? 0 : 1 })
|
await httpPost(`/user/enterprise/showEnterprise?id=${row.id}`)
|
||||||
ElMessage.success(row.showFlag === 1 ? '已显示' : '已隐藏'); load()
|
ElMessage.success(row.showFlag === 1 ? '已显示' : '已隐藏'); load()
|
||||||
break
|
break
|
||||||
case 'homeShow':
|
case 'homeShow':
|
||||||
await httpPost('/user/enterprise/homeShow', { id: row.id, homeShow: row.homeShow === 1 ? 0 : 1 })
|
await httpGet('/user/enterprise/homeShow', { id: row.id, homeShow: row.homeShow === 1 ? 0 : 1 })
|
||||||
ElMessage.success(row.homeShow === 1 ? '已取消推荐' : '已推荐'); load()
|
ElMessage.success(row.homeShow === 1 ? '已推荐' : '已取消推荐'); load()
|
||||||
break
|
break
|
||||||
case 'assign': ElMessage.info('分配业务员待实现'); break
|
case 'assign': ElMessage.info('分配业务员待实现'); break
|
||||||
case 'deleteRow':
|
case 'deleteRow':
|
||||||
@ -232,7 +335,7 @@ onMounted(() => load())
|
|||||||
:fields="[
|
:fields="[
|
||||||
{ key: 'enterpriseName', label: '企业名称', type: 'text', placeholder: '企业名称' },
|
{ key: 'enterpriseName', label: '企业名称', type: 'text', placeholder: '企业名称' },
|
||||||
{ key: 'industry', label: '行业类型', type: 'text', placeholder: '行业类型' },
|
{ key: 'industry', label: '行业类型', type: 'text', placeholder: '行业类型' },
|
||||||
{ key: 'supplierLevel', label: '供应商等级', type: 'select', placeholder: '供应商等级',
|
{ key: 'supplierLevel', label: '供应商等级', type: 'select', placeholder: '全部等级',
|
||||||
options: [
|
options: [
|
||||||
{ value: 1, label: '优质供应商' },
|
{ value: 1, label: '优质供应商' },
|
||||||
{ value: 2, label: '钻石供应商' },
|
{ value: 2, label: '钻石供应商' },
|
||||||
@ -240,6 +343,13 @@ onMounted(() => load())
|
|||||||
{ value: 4, label: '银牌供应商' },
|
{ value: 4, label: '银牌供应商' },
|
||||||
{ value: 5, label: '铜牌供应商' }
|
{ value: 5, label: '铜牌供应商' }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{ key: 'status', label: '审核状态', type: 'select', placeholder: '全部状态',
|
||||||
|
options: [
|
||||||
|
{ value: 1, label: '待审核' },
|
||||||
|
{ value: 2, label: '已认证' },
|
||||||
|
{ value: 3, label: '已驳回' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
@search="onSearch"
|
@search="onSearch"
|
||||||
@ -247,26 +357,32 @@ onMounted(() => load())
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<!-- 批量操作栏 -->
|
<!-- 批量操作栏 (选中时高亮) -->
|
||||||
<div class="rr-batch-bar">
|
<div class="rr-batch-bar" :class="{ 'has-selection': selectedCount > 0 }">
|
||||||
<el-button size="small" type="success" @click="importHandle">导入</el-button>
|
<span v-if="selectedCount > 0" class="batch-count">
|
||||||
<el-button size="small" type="danger" @click="batchAuditHandle">批量审核</el-button>
|
已选 <strong>{{ selectedCount }}</strong> 家企业
|
||||||
<el-button size="small" @click="batchLabelHandle">批量设置标签</el-button>
|
</span>
|
||||||
<el-button size="small" type="warning" @click="batchAssignSalesperson">批量分配业务员</el-button>
|
<span v-else class="batch-hint">勾选企业后可执行批量操作</span>
|
||||||
<el-button size="small" type="danger" @click="deleteHandle">删除</el-button>
|
<span class="batch-actions">
|
||||||
<el-button size="small" type="primary" @click="exportSupplierDetail">导出供应商信息明细</el-button>
|
<el-button size="small" type="primary" @click="openBatchAudit">批量审核</el-button>
|
||||||
|
<el-button size="small" @click="batchLabelHandle">批量设置标签</el-button>
|
||||||
|
<el-button size="small" type="warning" plain @click="batchAssignSalesperson">批量分配业务员</el-button>
|
||||||
|
<el-button size="small" @click="importHandle"><el-icon style="margin-right:4px"><Upload /></el-icon>导入</el-button>
|
||||||
|
<el-button size="small" @click="exportSupplierDetail"><el-icon style="margin-right:4px"><Download /></el-icon>导出明细</el-button>
|
||||||
|
<el-button size="small" type="danger" plain @click="deleteHandle">删除</el-button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
:columns="[
|
:columns="[
|
||||||
{ type: 'selection', width: 48, label: '' },
|
{ type: 'selection', width: 48, label: '' },
|
||||||
{ label: '企业名称', minWidth: 220, slot: 'enterpriseName' },
|
{ label: '企业名称', minWidth: 250, slot: 'enterpriseName' },
|
||||||
{ label: '企业管理员/手机号', minWidth: 200, slot: 'adminInfo' },
|
{ label: '企业管理员', minWidth: 170, slot: 'adminInfo' },
|
||||||
{ label: '行业类型/供应商等级', minWidth: 200, slot: 'levelInfo' },
|
{ label: '行业 / 供应商等级', minWidth: 190, slot: 'levelInfo' },
|
||||||
{ prop: 'supplierLabel', label: '标签', width: 120, align: 'center' },
|
{ label: '标签', minWidth: 140, slot: 'supplierLabel' },
|
||||||
{ label: '业务员', minWidth: 160, slot: 'salespersonInfo' },
|
{ label: '业务员', minWidth: 150, slot: 'salespersonInfo' },
|
||||||
{ label: '状态', width: 100, slot: 'status' },
|
{ label: '状态', width: 100, slot: 'status', align: 'center' },
|
||||||
{ label: '入驻时间', width: 110, slot: 'createDate' },
|
{ label: '入驻时间', width: 120, slot: 'createDate' },
|
||||||
{ label: '操作', width: 200, slot: 'action', fixed: 'right', align: 'center' }
|
{ label: '操作', width: 200, slot: 'action', fixed: 'right', align: 'center' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list"
|
||||||
@ -282,71 +398,91 @@ onMounted(() => load())
|
|||||||
@limit-change="onLimitChange"
|
@limit-change="onLimitChange"
|
||||||
@selection-change="onSelectionChange"
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<!-- 企业名称(带头像) -->
|
<!-- 企业名称 (头像 + 名称 + 法人) -->
|
||||||
<template #enterpriseName="{ row, index }">
|
<template #enterpriseName="{ row, index }">
|
||||||
<div style="display:flex;align-items:center;gap:8px">
|
<div class="ent-cell">
|
||||||
<span :style="{ display:'inline-flex',alignItems:'center',justifyContent:'center',
|
<span class="ent-avatar" :style="avatarStyle(row, index)">
|
||||||
width:'32px',height:'32px',borderRadius:'50%',flexShrink:0,fontSize:'14px',fontWeight:500,
|
<el-image v-if="row.enterpriseLogo" :src="row.enterpriseLogo" fit="cover" class="ent-avatar-img" />
|
||||||
background:index%2===0?'#fce4ec':'#e3f2fd',color:index%2===0?'#c62828':'#1565c0'
|
<template v-else>{{ initial(row.enterpriseName) }}</template>
|
||||||
}">{{ initial(row.enterpriseName) }}</span>
|
</span>
|
||||||
<span style="font-weight:500">{{ row.enterpriseName }}</span>
|
<span class="ent-text">
|
||||||
|
<span class="ent-name">{{ row.enterpriseName }}</span>
|
||||||
|
<span class="ent-sub" v-if="row.legalMan && row.legalMan !== '--'">法人:{{ row.legalMan }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 企业管理员/手机号(两行) -->
|
<!-- 企业管理员/手机号 (两行) -->
|
||||||
<template #adminInfo="{ row }">
|
<template #adminInfo="{ row }">
|
||||||
<div style="display:flex;flex-direction:column;gap:2px">
|
<div class="two-line">
|
||||||
<span>{{ row.administrator }}</span>
|
<span class="line-main">{{ row.administrator }}</span>
|
||||||
<span style="font-size:12px;color:var(--ink-3)">{{ maskMobile(row.administratorMobile) }}</span>
|
<span class="line-sub"><el-icon><User /></el-icon>{{ maskMobile(row.administratorMobile) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 行业类型/供应商等级(两行) -->
|
<!-- 行业/供应商等级 (两行 + 等级徽章) -->
|
||||||
<template #levelInfo="{ row }">
|
<template #levelInfo="{ row }">
|
||||||
<div style="display:flex;flex-direction:column;gap:2px">
|
<div class="two-line">
|
||||||
<span>{{ row.industry || '-' }}</span>
|
<span class="line-main">{{ row.industry || '-' }}</span>
|
||||||
<span style="font-size:12px;color:var(--ink-3)">{{ LEVEL_MAP[row.supplierLevel as number] || '-' }}</span>
|
<span v-if="LEVEL_META[row.supplierLevel as number]" class="level-chip"
|
||||||
|
:style="{ color: LEVEL_META[row.supplierLevel as number].color, background: LEVEL_META[row.supplierLevel as number].bg }">
|
||||||
|
{{ LEVEL_META[row.supplierLevel as number].label }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="line-sub">未评级</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 标签 (前 2 个 + 溢出) -->
|
||||||
|
<template #supplierLabel="{ row }">
|
||||||
|
<div class="label-tags" v-if="row.supplierLabel">
|
||||||
|
<span v-for="(t, i) in shownTags(row).tags" :key="i" class="label-tag">{{ t }}</span>
|
||||||
|
<span v-if="shownTags(row).rest > 0" class="label-more">+{{ shownTags(row).rest }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="rr-muted">--</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 业务员 -->
|
<!-- 业务员 -->
|
||||||
<template #salespersonInfo="{ row }">
|
<template #salespersonInfo="{ row }">
|
||||||
<div v-if="row.salesperson" style="font-size:12px;color:var(--ink-3)">
|
<div v-if="row.salesperson" class="two-line">
|
||||||
{{ row.salesperson }}({{ maskMobile(row.salespersonMobile) }})
|
<span class="line-main">{{ row.salesperson }}</span>
|
||||||
|
<span class="line-sub">{{ maskMobile(row.salespersonMobile) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="font-size:12px;color:var(--ink-3);font-style:italic">暂未分配</div>
|
<span v-else class="unassigned">待分配</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 状态标签 -->
|
<!-- 状态标签 (圆点 + 脉冲待审核) -->
|
||||||
<template #status="{ row }">
|
<template #status="{ row }">
|
||||||
<span v-if="STATUS_MAP[String(row.status)]" :class="['rr-status-tag', STATUS_CLASS[String(row.status)] || '']">
|
<span v-if="STATUS_META[String(row.status)]" class="status-tag" :class="{ pulsing: row.status === 1 }"
|
||||||
{{ STATUS_MAP[String(row.status)] }}
|
:style="{ color: STATUS_META[String(row.status)].color, background: STATUS_META[String(row.status)].bg }">
|
||||||
|
<span class="status-dot"></span>{{ STATUS_META[String(row.status)].label }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="rr-muted">--</span>
|
<span v-else class="rr-muted">--</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 入驻时间 -->
|
<!-- 入驻时间 -->
|
||||||
<template #createDate="{ row }">
|
<template #createDate="{ row }">
|
||||||
<span style="font-family:'SF Mono',Menlo,monospace;font-size:12px">{{ fmtDate(row.createDate) }}</span>
|
<span class="date-cell"><el-icon><Clock /></el-icon>{{ fmtDate(row.createDate) }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 操作列 -->
|
<!-- 操作列 -->
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<div style="display:flex;align-items:center;gap:2px">
|
<div class="action-cell">
|
||||||
<el-button type="primary" size="small" text @click="goDetail(row)">详情</el-button>
|
<el-button type="primary" size="small" text @click="goDetail(row)">详情</el-button>
|
||||||
<el-button type="primary" size="small" text @click="changeAdminHandle(row)">更换管理员</el-button>
|
<el-button size="small" text @click="changeAdminHandle(row)">更换管理员</el-button>
|
||||||
<el-dropdown trigger="click" placement="bottom-end" @command="(cmd:string) => handleMore(cmd, row)">
|
<el-dropdown trigger="click" placement="bottom-end" @command="(cmd:string) => handleMore(cmd, row)">
|
||||||
<el-button type="primary" size="small" text><el-icon><MoreFilled /></el-icon></el-button>
|
<el-button size="small" text><el-icon><MoreFilled /></el-icon></el-button>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item v-if="row.status===1" command="audit">审核</el-dropdown-item>
|
<el-dropdown-item v-if="row.status===1" command="audit">
|
||||||
|
<span class="dd-audit">审核</span>
|
||||||
|
</el-dropdown-item>
|
||||||
<el-dropdown-item command="label">标签</el-dropdown-item>
|
<el-dropdown-item command="label">标签</el-dropdown-item>
|
||||||
<el-dropdown-item command="rating">评级</el-dropdown-item>
|
<el-dropdown-item command="rating">评级</el-dropdown-item>
|
||||||
<el-dropdown-item command="enabled">{{ row.enabled===1?'启用':'禁用' }}</el-dropdown-item>
|
<el-dropdown-item divided command="enabled">{{ row.enabled===1?'启用':'禁用' }}</el-dropdown-item>
|
||||||
<el-dropdown-item command="showFlag">{{ row.showFlag===1?'显示企业':'隐藏企业' }}</el-dropdown-item>
|
<el-dropdown-item command="showFlag">{{ row.showFlag===1?'显示企业':'隐藏企业' }}</el-dropdown-item>
|
||||||
<el-dropdown-item command="homeShow">{{ row.homeShow===1?'取消推荐':'推荐显示' }}</el-dropdown-item>
|
<el-dropdown-item command="homeShow">{{ row.homeShow===1?'取消推荐':'推荐显示' }}</el-dropdown-item>
|
||||||
<el-dropdown-item command="assign">分配业务员</el-dropdown-item>
|
<el-dropdown-item command="assign">分配业务员</el-dropdown-item>
|
||||||
<el-dropdown-item command="deleteRow">删除</el-dropdown-item>
|
<el-dropdown-item divided command="deleteRow" class="dd-danger">删除</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@ -355,31 +491,67 @@ onMounted(() => load())
|
|||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详情弹窗 (EnterpriseDTO 全字段) -->
|
<!-- 单行审核弹窗 -->
|
||||||
<el-dialog v-model="detailVisible" title="企业详情" width="700px" :close-on-click-modal="false" destroy-on-close>
|
<el-dialog v-model="auditVisible" title="企业认证审核" width="480px" :close-on-click-modal="false" destroy-on-close>
|
||||||
<template v-if="detailRow">
|
<div style="background:var(--bg,#f6f8fb);border:1px solid var(--border);border-radius:8px;padding:10px 14px;font-size:13px;color:var(--ink-2);margin-bottom:18px">
|
||||||
<el-descriptions :column="2" border size="small">
|
正在审核:<strong style="color:var(--ink)">{{ auditForm.id ? (list.find(r => r.id === auditForm.id)?.enterpriseName || '--') : '--' }}</strong>
|
||||||
<el-descriptions-item label="企业名称">{{ detailRow.enterpriseName }}</el-descriptions-item>
|
</div>
|
||||||
<el-descriptions-item label="管理员">{{ detailRow.administrator }}</el-descriptions-item>
|
<el-form label-width="80px" label-position="left">
|
||||||
<el-descriptions-item label="管理员手机">{{ maskMobile(detailRow.administratorMobile) }}</el-descriptions-item>
|
<el-form-item label="审核结果">
|
||||||
<el-descriptions-item label="行业">{{ detailRow.industry || '-' }}</el-descriptions-item>
|
<el-radio-group v-model="auditForm.result">
|
||||||
<el-descriptions-item label="供应商等级">{{ LEVEL_MAP[detailRow.supplierLevel as number] || '-' }}</el-descriptions-item>
|
<el-radio :value="2">审核通过</el-radio>
|
||||||
<el-descriptions-item label="标签">{{ detailRow.supplierLabel || '-' }}</el-descriptions-item>
|
<el-radio :value="3">审核驳回</el-radio>
|
||||||
<el-descriptions-item label="业务员">{{ detailRow.salesperson || '暂未分配' }}</el-descriptions-item>
|
</el-radio-group>
|
||||||
<el-descriptions-item label="状态">{{ STATUS_MAP[String(detailRow.status)] || '-' }}</el-descriptions-item>
|
</el-form-item>
|
||||||
<el-descriptions-item v-if="detailRow.status===3" label="驳回原因" :span="2">{{ detailRow.failReason || '-' }}</el-descriptions-item>
|
<el-form-item v-if="auditForm.result === 3" label="驳回原因" required>
|
||||||
<el-descriptions-item label="入驻时间">{{ fmtDate(detailRow.createDate) }}</el-descriptions-item>
|
<el-input v-model="auditForm.failReason" type="textarea" :rows="4"
|
||||||
<el-descriptions-item label="法人代表">{{ detailRow.legalMan }}</el-descriptions-item>
|
placeholder="请填写驳回原因,将随站内消息与短信通知企业" maxlength="200" show-word-limit />
|
||||||
<el-descriptions-item label="联系电话">{{ maskMobile(detailRow.linkMobile) }}</el-descriptions-item>
|
</el-form-item>
|
||||||
<el-descriptions-item label="邮箱">{{ detailRow.email || '-' }}</el-descriptions-item>
|
</el-form>
|
||||||
<el-descriptions-item label="地址" :span="2">{{ detailRow.enterpriseAddress || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="主营产品" :span="2">{{ detailRow.mainProducts || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="工商号">{{ detailRow.orgNo || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="注册时间">{{ fmtDate(detailRow.registerTime) }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="detailVisible=false">关闭</el-button>
|
<el-button @click="auditVisible=false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="auditBusy" @click="submitAudit">确定提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 批量审核弹窗 -->
|
||||||
|
<el-dialog v-model="batchAuditVisible" title="批量审核企业" width="480px" :close-on-click-modal="false" destroy-on-close>
|
||||||
|
<el-form label-width="80px" label-position="left">
|
||||||
|
<el-form-item label="审核结果">
|
||||||
|
<el-radio-group v-model="batchAuditForm.result">
|
||||||
|
<el-radio :value="2">全部审核通过</el-radio>
|
||||||
|
<el-radio :value="3">全部驳回</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="batchAuditForm.result === 3" label="驳回原因" required>
|
||||||
|
<el-input v-model="batchAuditForm.failReason" type="textarea" :rows="4"
|
||||||
|
placeholder="请填写驳回原因,将随站内消息与短信通知企业" maxlength="200" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else label="通过说明">
|
||||||
|
<div style="font-size:12px;color:var(--ink-3);line-height:1.7;background:var(--bg,#f6f8fb);border-radius:8px;padding:10px 12px;width:100%">
|
||||||
|
已选 <strong style="color:var(--primary)">{{ selectedIds.length }}</strong> 家企业将通过认证,并通过站内消息与短信通知企业。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="batchAuditVisible=false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="batchAuditBusy" @click="submitBatchAudit">确定提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 更换管理员弹窗 -->
|
||||||
|
<el-dialog v-model="adminDialogVisible" title="更换管理员" width="420px" :close-on-click-modal="false" destroy-on-close>
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="管理员姓名" required>
|
||||||
|
<el-input v-model="adminForm.administrator" placeholder="请输入新管理员姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" required>
|
||||||
|
<el-input v-model="adminForm.administratorMobile" placeholder="请输入新管理员手机号" maxlength="11" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="adminDialogVisible=false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitChangeAdmin">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@ -390,15 +562,82 @@ onMounted(() => load())
|
|||||||
.rr-list-card {
|
.rr-list-card {
|
||||||
background: var(--surface); border-radius: 12px; overflow: hidden;
|
background: var(--surface); border-radius: 12px; overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 批量操作栏 ===== */
|
||||||
.rr-batch-bar {
|
.rr-batch-bar {
|
||||||
display: flex; gap: 8px; flex-wrap: wrap; padding: 12px 16px;
|
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
transition: background .2s;
|
||||||
}
|
}
|
||||||
.rr-status-tag {
|
.rr-batch-bar.has-selection { background: var(--primary-soft, #e0ecff); }
|
||||||
display: inline-block; padding: 2px 10px; border-radius: 4px; font-size: 12px; font-weight: 500;
|
.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; }
|
||||||
|
|
||||||
|
/* ===== 企业名称 ===== */
|
||||||
|
.ent-cell { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.ent-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;
|
||||||
}
|
}
|
||||||
.rr-tag-warn { background: #fff3e0; color: #e65100; }
|
.ent-avatar-img { width: 100%; height: 100%; border-radius: 10px; }
|
||||||
.rr-tag-ok { background: #e8f5e9; color: #2e7d32; }
|
.ent-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||||
.rr-tag-err { background: #fce4ec; color: #c62828; }
|
.ent-name { font-size: 14px; font-weight: 600; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.ent-sub { font-size: 12px; color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
|
/* ===== 两行单元格 ===== */
|
||||||
|
.two-line { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||||||
|
.line-main { font-size: 13px; color: var(--ink); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.line-sub { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--ink-3); }
|
||||||
|
|
||||||
|
/* ===== 等级徽章 ===== */
|
||||||
|
.level-chip {
|
||||||
|
display: inline-flex; align-items: center; width: fit-content;
|
||||||
|
font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 标签 ===== */
|
||||||
|
.label-tags { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
|
.label-tag {
|
||||||
|
font-size: 11px; color: var(--primary); background: var(--primary-soft, #e0ecff);
|
||||||
|
padding: 2px 10px; border-radius: 6px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.label-more { font-size: 11px; color: var(--ink-3); align-self: center; }
|
||||||
|
|
||||||
|
/* ===== 业务员 ===== */
|
||||||
|
.unassigned {
|
||||||
|
display: inline-flex; font-size: 11px; color: var(--ink-3);
|
||||||
|
background: var(--bg, #f6f8fb); border: 1px dashed var(--border);
|
||||||
|
padding: 2px 10px; border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 状态标签 ===== */
|
||||||
|
.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; }
|
||||||
|
.status-tag.pulsing .status-dot { animation: pulse-dot 1.6s ease-in-out infinite; }
|
||||||
|
@keyframes pulse-dot {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(180, 83, 9, .4); }
|
||||||
|
50% { box-shadow: 0 0 0 5px rgba(180, 83, 9, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 入驻时间 ===== */
|
||||||
|
.date-cell { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--ink-2); font-family: 'SF Mono', Menlo, Consolas, monospace; }
|
||||||
|
|
||||||
|
/* ===== 操作列 ===== */
|
||||||
|
.action-cell { display: flex; align-items: center; gap: 2px; }
|
||||||
|
.dd-audit { color: #b45309; font-weight: 600; }
|
||||||
|
.dd-danger { color: #dc2626; }
|
||||||
|
|
||||||
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
</style>
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.batch-actions { margin-left: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -88,10 +88,16 @@ watch(
|
|||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (v) => {
|
async (v) => {
|
||||||
if (!v) return
|
if (!v) return
|
||||||
|
// 2026-08-18 修复"先黑一下": loading 提前置 true 覆盖整个加载过程,
|
||||||
|
// 企业下拉与用户详情并行加载, 避免空表单闪现 + 后半程才出现遮罩
|
||||||
|
loading.value = true
|
||||||
resetForm()
|
resetForm()
|
||||||
await loadEnterprises()
|
try {
|
||||||
if (props.userId) {
|
const jobs: Promise<void>[] = [loadEnterprises()]
|
||||||
await loadDetail()
|
if (props.userId) jobs.push(loadDetail())
|
||||||
|
await Promise.all(jobs)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -213,7 +219,12 @@ async function submit() {
|
|||||||
@update:model-value="(v: boolean) => emit('update:visible', v)"
|
@update:model-value="(v: boolean) => emit('update:visible', v)"
|
||||||
@closed="resetForm"
|
@closed="resetForm"
|
||||||
>
|
>
|
||||||
<div v-loading="loading">
|
<!-- 透明遮罩 + 加载文案: 消除"先黑一下"观感 (暗色主题下黑遮罩叠加更明显) -->
|
||||||
|
<div
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="数据加载中…"
|
||||||
|
element-loading-background="transparent"
|
||||||
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="form"
|
:model="form"
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 用户管理 (前台用户列表) - W17 设计规范 v2 完整版
|
* F006 用户管理 (前台用户列表) - 2026-08-18 v2 简化版
|
||||||
*
|
*
|
||||||
* 按 ui-preview-v2.html 模板: 9 列 + 6 搜索 + 4 操作 + 状态 tab
|
* 用户反馈: 列表内容简化 - 单行显示、无头像、字段保持原样 (仅 UI 美化)
|
||||||
|
* 字段: 用户昵称 / 手机号 / 是否企业认证 / 关联企业 / 平台账户余额 / 禁用标识 / 注册时间 / 操作 (用户ID 已按需求隐藏)
|
||||||
|
* 后端端点: GET /user/user/page / GET /user/user/export / POST /user/user/update / POST /user/user/delete
|
||||||
|
* 保留功能: tab 全部/正常/已禁用(stats 与 tab 解耦) / 搜索 / 导出 / 新建编辑(UserFormDialog) / 批量操作
|
||||||
*/
|
*/
|
||||||
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)
|
||||||
@ -44,7 +49,6 @@ const exporting = ref(false)
|
|||||||
const query = reactive({
|
const query = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
userId: '',
|
|
||||||
nickName: '',
|
nickName: '',
|
||||||
mobile: '',
|
mobile: '',
|
||||||
enterpriseName: '',
|
enterpriseName: '',
|
||||||
@ -52,49 +56,32 @@ const query = reactive({
|
|||||||
enabled: '' as '' | '0' | '1'
|
enabled: '' as '' | '0' | '1'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 状态/认证胶囊色 (引用全局 --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[] = [
|
const columns: any[] = [
|
||||||
{ type: 'selection', width: 50, fixed: 'left', align: 'center' },
|
{ type: 'selection', width: 48, fixed: 'left', align: 'center' },
|
||||||
{ prop: 'id', label: '用户ID', width: 100, align: 'center' },
|
{ prop: 'nickName', label: '用户昵称', minWidth: 200, slot: 'nickName' },
|
||||||
{ prop: 'nickName', label: '用户昵称', minWidth: 160, align: 'center' },
|
{ prop: 'mobile', label: '手机号', width: 140, slot: 'mobile' },
|
||||||
{ prop: 'mobile', label: '手机号', width: 140, align: 'center' },
|
{ prop: 'isEnterprise', label: '是否企业认证', width: 120, slot: 'cert', align: 'center' },
|
||||||
{
|
{ prop: 'enterpriseName', label: '关联企业', minWidth: 180, slot: 'enterpriseName' },
|
||||||
prop: 'isEnterprise',
|
{ prop: 'accountBalance', label: '平台账户余额', width: 150, slot: 'balance', align: 'right' },
|
||||||
label: '是否企业认证',
|
{ prop: 'enabled', label: '禁用标识', width: 100, slot: 'enabled', align: 'center' },
|
||||||
width: 140,
|
{ label: '注册时间', width: 165, slot: 'createDate' },
|
||||||
align: 'center',
|
{ label: '操作', width: 170, slot: 'action', fixed: 'right', 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' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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))
|
||||||
@ -104,12 +91,10 @@ function fmtDate(ts: number | null | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建除 enabled (tab) 外的搜索参数 (用于 stats 查询)
|
* 构建除 enabled (tab) 外的搜索参数 (用于 stats 查询) — stats 与 tab 解耦
|
||||||
* stats 必须与 tab 解耦, 只反映当前搜索条件下的全量计数
|
|
||||||
*/
|
*/
|
||||||
function buildSearchParams() {
|
function buildSearchParams() {
|
||||||
const params: any = {}
|
const params: any = {}
|
||||||
if (query.userId) params.id = query.userId
|
|
||||||
if (query.nickName) params.nickName = query.nickName
|
if (query.nickName) params.nickName = query.nickName
|
||||||
if (query.mobile) params.mobile = query.mobile
|
if (query.mobile) params.mobile = query.mobile
|
||||||
if (query.enterpriseName) params.enterpriseName = query.enterpriseName
|
if (query.enterpriseName) params.enterpriseName = query.enterpriseName
|
||||||
@ -152,11 +137,7 @@ async function loadList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 加载 stats (normal/disabled 全量计数) — 与 tab 无关 */
|
||||||
* 加载 stats (normal/disabled 全量计数)
|
|
||||||
* 与 tab 无关, 只跟搜索条件 (userId/nickName/mobile/isEnterprise/enterpriseName) 关联
|
|
||||||
* 通过 limit=1 + page=1 拿到 total (后端只 count 不返列表, 开销可忽略)
|
|
||||||
*/
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
const baseParams = buildSearchParams()
|
const baseParams = buildSearchParams()
|
||||||
@ -177,7 +158,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 }
|
||||||
@ -196,13 +176,15 @@ function onPageChange(p: number) { query.page = p; load() }
|
|||||||
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
function onSearch() { query.page = 1; load() }
|
function onSearch() { query.page = 1; load() }
|
||||||
function onReset() {
|
function onReset() {
|
||||||
query.userId = ''; query.nickName = ''; query.mobile = ''
|
query.nickName = ''; query.mobile = ''
|
||||||
query.enterpriseName = ''; query.isEnterprise = ''; query.enabled = ''
|
query.enterpriseName = ''; query.isEnterprise = ''; query.enabled = ''
|
||||||
activeTab.value = 'all'
|
activeTab.value = 'all'
|
||||||
query.page = 1
|
query.page = 1
|
||||||
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
|
||||||
@ -217,33 +199,20 @@ function onDialogSuccess() {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 导出当前搜索条件下的用户列表为 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 +224,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,25 +282,20 @@ 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" />
|
||||||
|
|
||||||
<SearchBar
|
<SearchBar
|
||||||
v-model:filters="query"
|
v-model:filters="query"
|
||||||
:fields="[
|
:fields="[
|
||||||
{ key: 'userId', label: '用户ID', type: 'text', placeholder: '用户ID' },
|
|
||||||
{ key: 'nickName', label: '用户昵称', type: 'text', placeholder: '用户昵称' },
|
{ key: 'nickName', label: '用户昵称', type: 'text', placeholder: '用户昵称' },
|
||||||
{ key: 'mobile', label: '手机号', type: 'text', placeholder: '手机号' },
|
{ key: 'mobile', label: '手机号', type: 'text', placeholder: '手机号' },
|
||||||
{ key: 'isEnterprise', label: '是否企业认证', type: 'select', placeholder: '所有状态', width: '160px',
|
{ key: 'isEnterprise', label: '是否企业认证', type: 'select', placeholder: '所有状态', width: '160px',
|
||||||
@ -325,39 +306,128 @@ 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>
|
>
|
||||||
|
<!-- 用户昵称: 单行 -->
|
||||||
|
<template #nickName="{ row }">
|
||||||
|
<span class="nick-cell">{{ row.nickName }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 手机号 -->
|
||||||
|
<template #mobile="{ row }">
|
||||||
|
<span class="phone-cell"><el-icon><Phone /></el-icon>{{ row.mobile }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 是否企业认证: 胶囊 -->
|
||||||
|
<template #cert="{ row }">
|
||||||
|
<span class="chip" :style="{ color: CERT_META[row.isEnterprise].color, background: CERT_META[row.isEnterprise].bg }">
|
||||||
|
{{ CERT_META[row.isEnterprise].label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 关联企业: 单行 -->
|
||||||
|
<template #enterpriseName="{ row }">
|
||||||
|
<span class="text-ellipsis">{{ row.enterpriseName || '--' }}</span>
|
||||||
|
</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; }
|
||||||
</style>
|
.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; }
|
||||||
|
|
||||||
|
/* ===== 单元格 (单行) ===== */
|
||||||
|
.nick-cell { font-size: 14px; font-weight: 600; color: var(--ink); }
|
||||||
|
.text-ellipsis {
|
||||||
|
display: inline-block; max-width: 100%;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
vertical-align: middle; font-size: 13px; color: var(--ink);
|
||||||
|
}
|
||||||
|
.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>
|
||||||
@ -1,21 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 企业详情页 - 独立页面
|
* F006 企业详情页 - 专业 UI 重构版 (2026-08-18)
|
||||||
*
|
*
|
||||||
* 参考老 vue user/enterprise/enterprise-detail.vue 设计 + 新系统主题规范
|
* 参考老 vue user/enterprise/enterprise-detail.vue 设计 + 新系统主题规范
|
||||||
* 数据源:
|
* 数据源:
|
||||||
* - GET /user/enterprise/getInfo?enterpriseId= 企业全量信息(80+字段)
|
* - GET /user/enterprise/getInfo?enterpriseId= 企业全量信息(80+字段, 含聚合评分/商品数/入驻时长)
|
||||||
* - GET /user/performancescorerecord/page?enterpriseId= 履约评分记录
|
* - GET /user/performancescorerecord/page?enterpriseId= 履约评分记录
|
||||||
* 3 Tab: 基本信息 / 资质认证 / 履约情况
|
* - GET /user/enterprise/checkStatus?id=&status=&failReason= 审核(2通过/3驳回, 支持批量逗号)
|
||||||
|
* - POST /user/enterprise/enabled?id= 禁用/启用切换
|
||||||
|
* - POST /user/enterprise/showEnterprise?id= 显示/隐藏切换
|
||||||
|
* - GET /user/enterprise/homeShow?id=&homeShow= 首页推荐切换
|
||||||
|
*
|
||||||
|
* 结构: 状态横幅(审核操作) → 头部信息卡 → 认证流程时间线 → 3Tab(基本信息/资质认证/履约情况)
|
||||||
|
* 审核留痕: 后端无审核历史表, 时间线展示「入驻提交时间(真实)+当前审核结果(真实)
|
||||||
|
* +本会话审核操作(前端本地记录, 不伪造历史)」
|
||||||
*/
|
*/
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Shop } from '@element-plus/icons-vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import {
|
||||||
|
Shop, OfficeBuilding, MapLocation, User, Coin,
|
||||||
|
WarningFilled, SuccessFilled, CircleCloseFilled, Clock, Medal, Star
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
const enterpriseId = computed(() => String(route.params.id || ''))
|
const enterpriseId = computed(() => String(route.params.id || ''))
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -24,15 +37,23 @@ const detail = ref<any>({})
|
|||||||
const otherList = ref<any[]>([])
|
const otherList = ref<any[]>([])
|
||||||
const performanceList = ref<any[]>([])
|
const performanceList = ref<any[]>([])
|
||||||
|
|
||||||
const IDENTITY_MAP: Record<number, string> = { 1: '供应商', 2: '生产厂商', 3: '渠道商', 4: '服务商' }
|
// ================= 状态/映射常量 (与列表页统一 5 级) =================
|
||||||
const STATUS_MAP: Record<number, { label: string; type: 'success' | 'warning' | 'danger' | 'info' }> = {
|
const STATUS_META: Record<number, { label: string; type: 'warning' | 'success' | 'danger'; icon: any }> = {
|
||||||
1: { label: '待审核', type: 'warning' },
|
1: { label: '待审核', type: 'warning', icon: WarningFilled },
|
||||||
2: { label: '已认证', type: 'success' },
|
2: { label: '已认证', type: 'success', icon: SuccessFilled },
|
||||||
3: { label: '已驳回', type: 'danger' }
|
3: { label: '已驳回', type: 'danger', icon: CircleCloseFilled }
|
||||||
|
}
|
||||||
|
const IDENTITY_MAP: Record<number, string> = { 1: '供应商', 2: '生产厂商', 3: '渠道商', 4: '服务商' }
|
||||||
|
const LEVEL_MAP: Record<number, string> = {
|
||||||
|
1: '优质供应商', 2: '钻石供应商', 3: '金牌供应商', 4: '银牌供应商', 5: '铜牌供应商'
|
||||||
}
|
}
|
||||||
const LEVEL_MAP: Record<number, string> = { 1: 'A级', 2: 'B级', 3: 'C级', 4: 'D级' }
|
|
||||||
|
|
||||||
const isCertified = computed(() => detail.value.status === 2)
|
const status = computed<number>(() => Number(detail.value.status ?? 0))
|
||||||
|
const isCertified = computed(() => status.value === 2)
|
||||||
|
const isPending = computed(() => status.value === 1)
|
||||||
|
const isRejected = computed(() => status.value === 3)
|
||||||
|
const statusMeta = computed(() => STATUS_META[status.value])
|
||||||
|
|
||||||
const logoText = computed(() => (detail.value.enterpriseName || '企').trim().charAt(0))
|
const logoText = computed(() => (detail.value.enterpriseName || '企').trim().charAt(0))
|
||||||
const registerYear = computed(() => {
|
const registerYear = computed(() => {
|
||||||
const t = detail.value.registerTime
|
const t = detail.value.registerTime
|
||||||
@ -49,7 +70,42 @@ const productList = computed<string[]>(() => {
|
|||||||
if (!raw) return []
|
if (!raw) return []
|
||||||
return String(raw).split(/[,,、;;\s]+/).map((s: string) => s.trim()).filter(Boolean)
|
return String(raw).split(/[,,、;;\s]+/).map((s: string) => s.trim()).filter(Boolean)
|
||||||
})
|
})
|
||||||
|
const addressLine = computed(() => [detail.value.provinceName, detail.value.cityName, detail.value.areaName].filter(Boolean).join(' '))
|
||||||
|
const tenure = computed(() => {
|
||||||
|
const y = detail.value.year, m = detail.value.month, d = detail.value.day
|
||||||
|
if (y === undefined && m === undefined && d === undefined) return ''
|
||||||
|
const parts: string[] = []
|
||||||
|
if (y) parts.push(`${y}年`); if (m) parts.push(`${m}个月`); if (d) parts.push(`${d}天`)
|
||||||
|
return parts.join(' ') || '--'
|
||||||
|
})
|
||||||
|
|
||||||
|
// ================= 本会话审核操作记录 (前端本地留痕, 后端无历史表) =================
|
||||||
|
const localAuditLogs = ref<{ time: string; operator: string; result: string; reason: string }[]>([])
|
||||||
|
const auditVisible = ref(false)
|
||||||
|
const auditBusy = ref(false)
|
||||||
|
const auditForm = reactive({ result: 2, failReason: '' })
|
||||||
|
|
||||||
|
const timelineItems = computed(() => {
|
||||||
|
const items: { kind: string; title: string; time: string; desc: string; operator?: string }[] = []
|
||||||
|
// ① 提交认证 (真实数据)
|
||||||
|
items.push({ kind: 'submit', title: '提交企业认证', time: fmtDate(detail.value.createDate), desc: '企业提交了企业认证申请资料' })
|
||||||
|
// ② 当前审核结果 (真实数据)
|
||||||
|
if (status.value === 1) items.push({ kind: 'pending', title: '等待审核', time: '进行中', desc: '运营人员正在审核,请耐心等待' })
|
||||||
|
else if (status.value === 2) items.push({ kind: 'pass', title: '审核通过', time: '已认证', desc: '企业认证通过,可正常开展业务' })
|
||||||
|
else if (status.value === 3) items.push({ kind: 'fail', title: '审核驳回', time: '未通过', desc: detail.value.failReason || '未填写驳回原因' })
|
||||||
|
// ③ 本会话内完成的审核操作 (本地追加)
|
||||||
|
for (const log of localAuditLogs.value) {
|
||||||
|
items.push({
|
||||||
|
kind: log.result.includes('通过') ? 'pass' : 'fail',
|
||||||
|
title: log.result, time: log.time,
|
||||||
|
desc: log.reason || '—',
|
||||||
|
operator: log.operator
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
// ================= 数据加载 =================
|
||||||
function maskMobile(m: string | number | null | undefined): string {
|
function maskMobile(m: string | number | null | undefined): string {
|
||||||
if (m === undefined || m === null || m === '') return '--'
|
if (m === undefined || m === null || m === '') return '--'
|
||||||
const s = String(m)
|
const s = String(m)
|
||||||
@ -63,6 +119,10 @@ function fmtMoney(n: any): string {
|
|||||||
const v = Number(n || 0)
|
const v = Number(n || 0)
|
||||||
return v ? `¥ ${v.toLocaleString()}` : '¥ 0'
|
return v ? `¥ ${v.toLocaleString()}` : '¥ 0'
|
||||||
}
|
}
|
||||||
|
function fmtNum(n: any): string {
|
||||||
|
const v = Number(n)
|
||||||
|
return Number.isFinite(v) ? String(v) : '--'
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -87,6 +147,72 @@ async function load() {
|
|||||||
} catch { performanceList.value = [] }
|
} catch { performanceList.value = [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= 审核交互 =================
|
||||||
|
function openAudit(defaultResult: number = 2) {
|
||||||
|
auditForm.result = defaultResult
|
||||||
|
auditForm.failReason = ''
|
||||||
|
auditVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAudit() {
|
||||||
|
if (auditForm.result === 3 && !auditForm.failReason.trim()) {
|
||||||
|
ElMessage.warning('请填写驳回原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
auditBusy.value = true
|
||||||
|
try {
|
||||||
|
await httpGet('/user/enterprise/checkStatus', {
|
||||||
|
id: enterpriseId.value,
|
||||||
|
status: auditForm.result,
|
||||||
|
failReason: auditForm.result === 3 ? auditForm.failReason.trim() : ''
|
||||||
|
})
|
||||||
|
const passed = auditForm.result === 2
|
||||||
|
ElMessage.success(passed ? '审核通过,企业已认证' : '已驳回,企业认证未通过')
|
||||||
|
auditVisible.value = false
|
||||||
|
// 本会话审计留痕
|
||||||
|
localAuditLogs.value.push({
|
||||||
|
time: fmtDate(new Date().toISOString()),
|
||||||
|
operator: userStore.displayName || '管理员',
|
||||||
|
result: passed ? '审核通过' : '审核驳回',
|
||||||
|
reason: passed ? '认可企业提交的认证资料' : auditForm.failReason.trim()
|
||||||
|
})
|
||||||
|
await load()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '审核操作失败')
|
||||||
|
} finally {
|
||||||
|
auditBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 状态切换 (后端均为 query 参数) =================
|
||||||
|
async function toggleEnabled() {
|
||||||
|
const disabling = detail.value.enabled !== 1
|
||||||
|
try { await ElMessageBox.confirm(`${disabling ? '禁用' : '启用'}后,该企业${disabling ? '将无法登录和使用平台服务' : '恢复正常使用'}。确认${disabling ? '禁用' : '启用'}?`, '操作确认', { type: 'warning' }) } catch { return }
|
||||||
|
try {
|
||||||
|
await httpPost(`/user/enterprise/enabled?id=${enterpriseId.value}`)
|
||||||
|
ElMessage.success(disabling ? '已禁用该企业' : '已启用该企业')
|
||||||
|
await load()
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleShow() {
|
||||||
|
const hiding = detail.value.showFlag !== 1
|
||||||
|
try {
|
||||||
|
await httpPost(`/user/enterprise/showEnterprise?id=${enterpriseId.value}`)
|
||||||
|
ElMessage.success(hiding ? '已隐藏企业(列表不再展示)' : '已恢复显示企业')
|
||||||
|
await load()
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleHome() {
|
||||||
|
const recommend = detail.value.homeShow === 1 ? 0 : 1
|
||||||
|
try {
|
||||||
|
await httpGet('/user/enterprise/homeShow', { id: enterpriseId.value, homeShow: recommend })
|
||||||
|
ElMessage.success(recommend === 1 ? '已推荐到首页' : '已取消首页推荐')
|
||||||
|
await load()
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
|
}
|
||||||
|
|
||||||
function goStore() {
|
function goStore() {
|
||||||
const eid = detail.value.enterpriseId || detail.value.id
|
const eid = detail.value.enterpriseId || detail.value.id
|
||||||
if (!eid) return
|
if (!eid) return
|
||||||
@ -99,10 +225,56 @@ onMounted(load)
|
|||||||
<template>
|
<template>
|
||||||
<div class="ent-detail" v-loading="loading">
|
<div class="ent-detail" v-loading="loading">
|
||||||
<PageHeader title="企业详情" :sub="`入驻企业信息总览`">
|
<PageHeader title="企业详情" :sub="`入驻企业信息总览`">
|
||||||
<el-button @click="router.back()">返回列表</el-button>
|
<el-button @click="router.back()">
|
||||||
|
<el-icon style="margin-right:4px"><Clock /></el-icon>返回列表
|
||||||
|
</el-button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<!-- ===== Header 区: logo + 企业名 + 认证标签 + meta ===== -->
|
<!-- ===== ① 状态横幅 (审核操作区) ===== -->
|
||||||
|
<div class="audit-banner" :class="`banner-${status || 'unknown'}`" v-if="status">
|
||||||
|
<div class="banner-icon">
|
||||||
|
<el-icon v-if="statusMeta" :size="26"><component :is="statusMeta.icon" /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="banner-main">
|
||||||
|
<div class="banner-title">
|
||||||
|
{{ isPending ? '企业认证待审核' : isCertified ? '企业认证已通过' : isRejected ? '企业认证未通过' : '状态未知' }}
|
||||||
|
</div>
|
||||||
|
<div class="banner-desc" v-if="isPending">
|
||||||
|
该企业已提交认证申请资料,请核查营业执照、法人证件及资质证书后给出审核结论。
|
||||||
|
</div>
|
||||||
|
<div class="banner-desc" v-else-if="isRejected">
|
||||||
|
驳回原因:<span class="reason-text">{{ detail.failReason || '未填写' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="banner-desc" v-else-if="isCertified">
|
||||||
|
企业已通过认证,可正常发布商品、参与询报价与交易。
|
||||||
|
</div>
|
||||||
|
<div class="banner-badges">
|
||||||
|
<el-tag v-if="detail.enabled === 1" type="danger" size="small" effect="dark" round>已禁用</el-tag>
|
||||||
|
<el-tag v-if="detail.showFlag === 1" type="info" size="small" effect="plain" round>已隐藏</el-tag>
|
||||||
|
<el-tag v-if="detail.homeShow === 1" type="success" size="small" effect="plain" round>首页推荐</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="banner-actions">
|
||||||
|
<template v-if="isPending">
|
||||||
|
<el-button type="primary" @click="openAudit(2)">
|
||||||
|
<el-icon style="margin-right:4px"><SuccessFilled /></el-icon>审核通过
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" plain @click="openAudit(3)">驳 回</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isRejected">
|
||||||
|
<el-button type="primary" @click="openAudit(2)">
|
||||||
|
<el-icon style="margin-right:4px"><SuccessFilled /></el-icon>重新审核
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isCertified">
|
||||||
|
<el-button size="small" :type="detail.enabled === 1 ? 'success' : 'danger'" plain @click="toggleEnabled">
|
||||||
|
{{ detail.enabled === 1 ? '启用企业' : '禁用企业' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== ② 头部信息卡 ===== -->
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<div class="header-logo">
|
<div class="header-logo">
|
||||||
<el-image v-if="detail.enterpriseLogo" :src="detail.enterpriseLogo" fit="cover" class="logo-img" />
|
<el-image v-if="detail.enterpriseLogo" :src="detail.enterpriseLogo" fit="cover" class="logo-img" />
|
||||||
@ -112,12 +284,12 @@ onMounted(load)
|
|||||||
<div class="header-title-row">
|
<div class="header-title-row">
|
||||||
<h2 class="ent-name">{{ detail.enterpriseName || '--' }}</h2>
|
<h2 class="ent-name">{{ detail.enterpriseName || '--' }}</h2>
|
||||||
<span v-for="(t, i) in tags" :key="i" class="name-tag">{{ t }}</span>
|
<span v-for="(t, i) in tags" :key="i" class="name-tag">{{ t }}</span>
|
||||||
<el-tag v-if="STATUS_MAP[detail.status]" :type="STATUS_MAP[detail.status].type" effect="light" round>{{ STATUS_MAP[detail.status].label }}</el-tag>
|
<el-tag v-if="statusMeta" :type="statusMeta.type" effect="light" round>{{ statusMeta.label }}</el-tag>
|
||||||
<el-tag v-if="detail.enabled === 1" type="info" effect="plain" round>已禁用</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="header-meta">
|
<div class="header-meta">
|
||||||
<span v-if="detail.industry" class="meta-item">{{ detail.industry }}</span>
|
<span v-if="detail.industry" class="meta-item">{{ detail.industry }}</span>
|
||||||
<span v-if="registerYear !== '--'" class="meta-item">{{ registerYear }} 成立</span>
|
<span v-if="registerYear !== '--'" class="meta-item">成立于 {{ registerYear }}</span>
|
||||||
|
<span v-if="tenure" class="meta-item">入驻 {{ tenure }}</span>
|
||||||
<span v-if="detail.creditCode" class="meta-item">信用代码 {{ detail.creditCode }}</span>
|
<span v-if="detail.creditCode" class="meta-item">信用代码 {{ detail.creditCode }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -133,15 +305,53 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-cell">
|
<div class="stat-cell">
|
||||||
<div class="stat-value">{{ detail.enterpriseRate ?? '--' }}</div>
|
<div class="stat-value">{{ detail.enterpriseRate ?? '--' }}</div>
|
||||||
<div class="stat-label">企业评级</div>
|
<div class="stat-label">好评率</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" class="store-btn" @click="goStore" :disabled="!detail.enterpriseId && !detail.id">
|
<div class="header-btns">
|
||||||
<el-icon style="margin-right:6px"><Shop /></el-icon>进入店铺
|
<el-button type="primary" class="store-btn" @click="goStore" :disabled="!detail.enterpriseId && !detail.id">
|
||||||
</el-button>
|
<el-icon style="margin-right:6px"><Shop /></el-icon>进入店铺
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown trigger="click" placement="bottom-end" v-if="isCertified">
|
||||||
|
<el-button plain>
|
||||||
|
更多操作<el-icon class="el-icon--right"><Coin /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item :command="1" @click="toggleShow">{{ detail.showFlag === 1 ? '恢复显示' : '隐藏企业' }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item :command="2" @click="toggleHome">{{ detail.homeShow === 1 ? '取消首页推荐' : '推荐到首页' }}</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== ③ 认证流程时间线 ===== -->
|
||||||
|
<div class="info-card timeline-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><Medal /></el-icon>
|
||||||
|
<span class="card-title-text">认证流程</span>
|
||||||
|
<span class="card-sub">入驻申请 · 审核结果 · 本会话操作记录</span>
|
||||||
|
</div>
|
||||||
|
<el-timeline class="audit-timeline">
|
||||||
|
<el-timeline-item
|
||||||
|
v-for="(item, i) in timelineItems"
|
||||||
|
:key="i"
|
||||||
|
:type="item.kind === 'pass' ? 'success' : item.kind === 'fail' ? 'danger' : item.kind === 'pending' ? 'warning' : 'primary'"
|
||||||
|
:hollow="item.kind === 'pending'"
|
||||||
|
:timestamp="item.time"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div class="tl-content">
|
||||||
|
<span class="tl-title">{{ item.title }}</span>
|
||||||
|
<span class="tl-desc">{{ item.desc }}</span>
|
||||||
|
<span v-if="item.operator" class="tl-operator">操作人:{{ item.operator }}</span>
|
||||||
|
</div>
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===== Tab 导航 ===== -->
|
<!-- ===== Tab 导航 ===== -->
|
||||||
<div class="tab-nav">
|
<div class="tab-nav">
|
||||||
<div class="tab-item" :class="{ active: activeTab === 'basic' }" @click="activeTab = 'basic'">基本信息</div>
|
<div class="tab-item" :class="{ active: activeTab === 'basic' }" @click="activeTab = 'basic'">基本信息</div>
|
||||||
@ -154,21 +364,27 @@ onMounted(load)
|
|||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-col">
|
<div class="info-col">
|
||||||
<section class="info-card">
|
<section class="info-card">
|
||||||
<div class="card-title">公司信息</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><OfficeBuilding /></el-icon>
|
||||||
|
<span class="card-title-text">公司信息</span>
|
||||||
|
</div>
|
||||||
<div class="info-grid-2">
|
<div class="info-grid-2">
|
||||||
<div class="info-item"><span class="label">公司名称</span><span class="value">{{ detail.enterpriseName || '--' }}</span></div>
|
<div class="info-item"><span class="label">公司名称</span><span class="value">{{ detail.enterpriseName || '--' }}</span></div>
|
||||||
<div class="info-item"><span class="label">注册时间</span><span class="value">{{ fmtDate(detail.registerTime) }}</span></div>
|
<div class="info-item"><span class="label">注册时间</span><span class="value">{{ fmtDate(detail.registerTime) }}</span></div>
|
||||||
<div class="info-item"><span class="label">所属行业</span><span class="value">{{ detail.industry || '--' }}</span></div>
|
<div class="info-item"><span class="label">所属行业</span><span class="value">{{ detail.industry || '--' }}</span></div>
|
||||||
<div class="info-item"><span class="label">企业身份</span><span class="value">{{ IDENTITY_MAP[detail.enterpriseIdentity as number] || detail.enterpriseIdentity || '--' }}</span></div>
|
<div class="info-item"><span class="label">企业身份</span><span class="value">{{ IDENTITY_MAP[detail.enterpriseIdentity as number] || detail.enterpriseIdentity || '--' }}</span></div>
|
||||||
<div class="info-item"><span class="label">企业类型</span><span class="value">{{ detail.enterpriseType ?? '--' }}</span></div>
|
<div class="info-item"><span class="label">企业类型</span><span class="value">{{ detail.enterpriseType ?? '--' }}</span></div>
|
||||||
<div class="info-item"><span class="label">统一信用代码</span><span class="value">{{ detail.creditCode || detail.orgNo || '--' }}</span></div>
|
<div class="info-item"><span class="label">统一信用代码</span><span class="value mono">{{ detail.creditCode || detail.orgNo || '--' }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="info-card">
|
<section class="info-card">
|
||||||
<div class="card-title">地址信息</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><MapLocation /></el-icon>
|
||||||
|
<span class="card-title-text">地址信息</span>
|
||||||
|
</div>
|
||||||
<div class="address-box">
|
<div class="address-box">
|
||||||
<div class="addr-line">{{ [detail.provinceName, detail.cityName, detail.areaName].filter(Boolean).join(' ') || '--' }}</div>
|
<div class="addr-line">{{ addressLine || '--' }}</div>
|
||||||
<div class="value">{{ detail.enterpriseAddress || '--' }}</div>
|
<div class="value">{{ detail.enterpriseAddress || '--' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -176,31 +392,29 @@ onMounted(load)
|
|||||||
|
|
||||||
<div class="info-col">
|
<div class="info-col">
|
||||||
<section class="info-card">
|
<section class="info-card">
|
||||||
<div class="card-title">联系人信息</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><User /></el-icon>
|
||||||
|
<span class="card-title-text">联系人信息</span>
|
||||||
|
</div>
|
||||||
<div class="contact-profile">
|
<div class="contact-profile">
|
||||||
<div class="contact-avatar">{{ contactInitial }}</div>
|
<div class="contact-avatar">{{ contactInitial }}</div>
|
||||||
<div class="contact-name">{{ detail.linkMan || '--' }}</div>
|
<div class="contact-name">{{ detail.linkMan || '--' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-methods">
|
<div class="contact-methods">
|
||||||
<div class="contact-method">
|
<div class="contact-method"><span class="label">联系电话</span><span class="value">{{ maskMobile(detail.linkMobile) }}</span></div>
|
||||||
<span class="label">联系电话</span><span class="value">{{ maskMobile(detail.linkMobile) }}</span>
|
<div class="contact-method"><span class="label">电子邮箱</span><span class="value">{{ detail.email || '--' }}</span></div>
|
||||||
</div>
|
<div class="contact-method"><span class="label">管理员</span><span class="value">{{ detail.administrator || '--' }} ({{ maskMobile(detail.administratorMobile) }})</span></div>
|
||||||
<div class="contact-method">
|
<div class="contact-method"><span class="label">业务员</span><span class="value">{{ detail.salesperson || '暂未分配' }}</span></div>
|
||||||
<span class="label">电子邮箱</span><span class="value">{{ detail.email || '--' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="contact-method">
|
|
||||||
<span class="label">管理员</span><span class="value">{{ detail.administrator || '--' }} ({{ maskMobile(detail.administratorMobile) }})</span>
|
|
||||||
</div>
|
|
||||||
<div class="contact-method">
|
|
||||||
<span class="label">业务员</span><span class="value">{{ detail.salesperson || '暂未分配' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="info-card full">
|
<section class="info-card full">
|
||||||
<div class="card-title">经营信息</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><Coin /></el-icon>
|
||||||
|
<span class="card-title-text">经营信息</span>
|
||||||
|
</div>
|
||||||
<div class="info-grid-2">
|
<div class="info-grid-2">
|
||||||
<div class="info-item span2"><span class="label">经营范围</span><span class="value">{{ detail.scope || detail.scopeOfLicence || '--' }}</span></div>
|
<div class="info-item span2"><span class="label">经营范围</span><span class="value">{{ detail.scope || detail.scopeOfLicence || '--' }}</span></div>
|
||||||
<div class="info-item span2"><span class="label">主营产品</span>
|
<div class="info-item span2"><span class="label">主营产品</span>
|
||||||
@ -209,10 +423,10 @@ onMounted(load)
|
|||||||
<span v-if="!productList.length" class="value">--</span>
|
<span v-if="!productList.length" class="value">--</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item"><span class="label">银行</span><span class="value">{{ detail.bankOfDeposit || '--' }}</span></div>
|
<div class="info-item"><span class="label">开户银行</span><span class="value">{{ detail.bankOfDeposit || '--' }}</span></div>
|
||||||
<div class="info-item"><span class="label">开户名称</span><span class="value">{{ detail.accountName || '--' }}</span></div>
|
|
||||||
<div class="info-item"><span class="label">账号</span><span class="value">{{ detail.accountCard || '--' }}</span></div>
|
|
||||||
<div class="info-item"><span class="label">账户余额</span><span class="value">{{ fmtMoney(detail.accountBalance) }}</span></div>
|
<div class="info-item"><span class="label">账户余额</span><span class="value">{{ fmtMoney(detail.accountBalance) }}</span></div>
|
||||||
|
<div class="info-item"><span class="label">开户名称</span><span class="value">{{ detail.accountName || '--' }}</span></div>
|
||||||
|
<div class="info-item"><span class="label">银行账号</span><span class="value mono">{{ detail.accountCard || '--' }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -220,21 +434,29 @@ onMounted(load)
|
|||||||
<!-- ===== Tab 2: 资质认证 ===== -->
|
<!-- ===== Tab 2: 资质认证 ===== -->
|
||||||
<div v-show="activeTab === 'qual'" class="tab-body">
|
<div v-show="activeTab === 'qual'" class="tab-body">
|
||||||
<section class="info-card full">
|
<section class="info-card full">
|
||||||
<div class="card-title">营业执照</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><DocumentChecked /></el-icon>
|
||||||
|
<span class="card-title-text">营业执照</span>
|
||||||
|
<span class="card-sub">点击图片可放大预览</span>
|
||||||
|
</div>
|
||||||
<el-image v-if="detail.licenseImg" :src="detail.licenseImg" fit="contain" class="cert-img" :preview-src-list="[detail.licenseImg]" preview-teleported />
|
<el-image v-if="detail.licenseImg" :src="detail.licenseImg" fit="contain" class="cert-img" :preview-src-list="[detail.licenseImg]" preview-teleported />
|
||||||
<div v-else class="cert-empty">暂无营业执照</div>
|
<div v-else class="cert-empty"><span>暂无营业执照</span></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="info-card full">
|
<section class="info-card full">
|
||||||
<div class="card-title">法人身份证</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><DocumentChecked /></el-icon>
|
||||||
|
<span class="card-title-text">法人身份证</span>
|
||||||
|
<span class="card-sub">点击图片可放大预览</span>
|
||||||
|
</div>
|
||||||
<div class="idcard-row">
|
<div class="idcard-row">
|
||||||
<div class="idcard-cell">
|
<div class="idcard-cell">
|
||||||
<span class="label">正面</span>
|
<span class="label">正面(人像面)</span>
|
||||||
<el-image v-if="detail.frontImage" :src="detail.frontImage" fit="cover" class="idcard-img" :preview-src-list="[detail.frontImage]" preview-teleported />
|
<el-image v-if="detail.frontImage" :src="detail.frontImage" fit="cover" class="idcard-img" :preview-src-list="[detail.frontImage]" preview-teleported />
|
||||||
<div v-else class="cert-empty small">暂无</div>
|
<div v-else class="cert-empty small">暂无</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="idcard-cell">
|
<div class="idcard-cell">
|
||||||
<span class="label">反面</span>
|
<span class="label">反面(国徽面)</span>
|
||||||
<el-image v-if="detail.backImage" :src="detail.backImage" fit="cover" class="idcard-img" :preview-src-list="[detail.backImage]" preview-teleported />
|
<el-image v-if="detail.backImage" :src="detail.backImage" fit="cover" class="idcard-img" :preview-src-list="[detail.backImage]" preview-teleported />
|
||||||
<div v-else class="cert-empty small">暂无</div>
|
<div v-else class="cert-empty small">暂无</div>
|
||||||
</div>
|
</div>
|
||||||
@ -243,33 +465,70 @@ onMounted(load)
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="info-card full">
|
<section class="info-card full">
|
||||||
<div class="card-title">资质证书</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><Medal /></el-icon>
|
||||||
|
<span class="card-title-text">资质证书</span>
|
||||||
|
<span class="card-sub" v-if="otherList.length">共 {{ otherList.length }} 项</span>
|
||||||
|
</div>
|
||||||
<template v-if="otherList.length">
|
<template v-if="otherList.length">
|
||||||
<div v-for="(cert, i) in otherList" :key="i" class="cert-card">
|
<div class="cert-card" v-for="(cert, i) in otherList" :key="i">
|
||||||
<div class="cert-info">
|
<div class="cert-main">
|
||||||
<div class="cert-row"><span class="label">资质名称</span><span class="value">{{ cert.qualificationName || '--' }}</span></div>
|
<div class="cert-info">
|
||||||
<div class="cert-row"><span class="label">证书编号</span><span class="value">{{ cert.certificateNumber || '--' }}</span></div>
|
<div class="cert-row"><span class="label">资质名称</span><span class="value">{{ cert.qualificationName || '--' }}</span></div>
|
||||||
<div class="cert-row"><span class="label">有效期至</span><span class="value">{{ cert.expirationDate || '--' }}</span></div>
|
<div class="cert-row"><span class="label">证书编号</span><span class="value mono">{{ cert.certificateNumber || '--' }}</span></div>
|
||||||
</div>
|
<div class="cert-row"><span class="label">有效期至</span>
|
||||||
<div v-if="cert.imageList?.length" class="cert-imgs">
|
<span class="value">{{ cert.expirationDate || '--' }}
|
||||||
<el-image v-for="(img, j) in cert.imageList" :key="j" :src="img" fit="cover" class="cert-thumb" :preview-src-list="cert.imageList" preview-teleported />
|
<el-tag v-if="cert.isExpiration === 1" type="danger" size="small" style="margin-left:6px">已过期</el-tag>
|
||||||
|
<el-tag v-else-if="cert.expirationDate" type="success" size="small" style="margin-left:6px">有效</el-tag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="cert.imageList?.length" class="cert-imgs">
|
||||||
|
<el-image v-for="(img, j) in cert.imageList" :key="j" :src="img" fit="cover" class="cert-thumb" :preview-src-list="cert.imageList" preview-teleported />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="cert-empty">暂无资质证书</div>
|
<div v-else class="cert-empty"><span>暂无资质证书</span></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="info-card full">
|
<section class="info-card full">
|
||||||
<div class="card-title">企业授权书</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><DocumentChecked /></el-icon>
|
||||||
|
<span class="card-title-text">企业授权书</span>
|
||||||
|
<span class="card-sub">点击图片可放大预览</span>
|
||||||
|
</div>
|
||||||
<el-image v-if="detail.enterpriseAccredit" :src="detail.enterpriseAccredit" fit="cover" class="cert-img tall" :preview-src-list="[detail.enterpriseAccredit]" preview-teleported />
|
<el-image v-if="detail.enterpriseAccredit" :src="detail.enterpriseAccredit" fit="cover" class="cert-img tall" :preview-src-list="[detail.enterpriseAccredit]" preview-teleported />
|
||||||
<div v-else class="cert-empty">暂无授权书</div>
|
<div v-else class="cert-empty"><span>暂无授权书</span></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== Tab 3: 履约情况 ===== -->
|
<!-- ===== Tab 3: 履约情况 ===== -->
|
||||||
<div v-show="activeTab === 'perf'" class="tab-body">
|
<div v-show="activeTab === 'perf'" class="tab-body">
|
||||||
|
<div class="kpi-grid">
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">企业评分</div>
|
||||||
|
<div class="kpi-value primary">{{ fmtNum(detail.enterpriseScore) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">好评率</div>
|
||||||
|
<div class="kpi-value">{{ fmtNum(detail.enterpriseRate) }}<span class="kpi-unit">%</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">评价总数</div>
|
||||||
|
<div class="kpi-value">{{ fmtNum(detail.enterpriseCommentNum) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">在售商品</div>
|
||||||
|
<div class="kpi-value">{{ fmtNum(detail.physicalGoodsNum) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="info-card full">
|
<section class="info-card full">
|
||||||
<div class="card-title">履约评分记录</div>
|
<div class="card-head">
|
||||||
|
<el-icon class="card-head-icon"><Star /></el-icon>
|
||||||
|
<span class="card-title-text">履约评分记录</span>
|
||||||
|
</div>
|
||||||
<template v-if="performanceList.length">
|
<template v-if="performanceList.length">
|
||||||
<div v-for="(item, i) in performanceList" :key="i" class="perf-item">
|
<div v-for="(item, i) in performanceList" :key="i" class="perf-item">
|
||||||
<span class="perf-date">{{ fmtDate(item.date) }}</span>
|
<span class="perf-date">{{ fmtDate(item.date) }}</span>
|
||||||
@ -277,24 +536,91 @@ onMounted(load)
|
|||||||
<span class="perf-score">{{ item.score ?? '--' }} 分</span>
|
<span class="perf-score">{{ item.score ?? '--' }} 分</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="cert-empty">暂无履约评分记录</div>
|
<div v-else class="perf-empty">暂无履约评分记录</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== 审核弹窗 ===== -->
|
||||||
|
<el-dialog v-model="auditVisible" title="企业认证审核" width="480px" :close-on-click-modal="false" destroy-on-close>
|
||||||
|
<div class="audit-dialog-tip">
|
||||||
|
正在审核:<strong>{{ detail.enterpriseName || '--' }}</strong>
|
||||||
|
</div>
|
||||||
|
<el-form label-width="80px" label-position="left">
|
||||||
|
<el-form-item label="审核结果">
|
||||||
|
<el-radio-group v-model="auditForm.result">
|
||||||
|
<el-radio :value="2">审核通过</el-radio>
|
||||||
|
<el-radio :value="3">审核驳回</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="auditForm.result === 3" label="驳回原因" required>
|
||||||
|
<el-input
|
||||||
|
v-model="auditForm.failReason"
|
||||||
|
type="textarea" :rows="4"
|
||||||
|
placeholder="请填写驳回原因,将随站内消息与短信通知企业"
|
||||||
|
maxlength="200" show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else label="通过说明">
|
||||||
|
<div class="pass-tip">审核通过后将自动:完成企业认证、初始化审批流、创建默认分类与默认支付方式,并通过站内消息与短信通知企业。</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="auditVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="auditBusy" @click="submitAudit">确定提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.ent-detail { max-width: 1400px; }
|
.ent-detail { max-width: 1400px; }
|
||||||
|
|
||||||
/* ===== Header ===== */
|
/* ===== ① 状态横幅 ===== */
|
||||||
|
.audit-banner {
|
||||||
|
display: flex; align-items: center; gap: 18px;
|
||||||
|
border-radius: var(--radius-lg); padding: 16px 22px; margin-bottom: 16px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.banner-warning {
|
||||||
|
background: linear-gradient(90deg, rgba(245,158,11,.14), rgba(245,158,11,.04));
|
||||||
|
border-color: rgba(245,158,11,.35);
|
||||||
|
.banner-icon { background: rgba(245,158,11,.18); color: #b45309; }
|
||||||
|
}
|
||||||
|
.banner-success {
|
||||||
|
background: linear-gradient(90deg, rgba(34,197,94,.12), rgba(34,197,94,.03));
|
||||||
|
border-color: rgba(34,197,94,.3);
|
||||||
|
.banner-icon { background: rgba(34,197,94,.18); color: #15803d; }
|
||||||
|
}
|
||||||
|
.banner-danger {
|
||||||
|
background: linear-gradient(90deg, rgba(239,68,68,.12), rgba(239,68,68,.03));
|
||||||
|
border-color: rgba(239,68,68,.32);
|
||||||
|
.banner-icon { background: rgba(239,68,68,.16); color: #b91c1c; }
|
||||||
|
}
|
||||||
|
.banner-unknown {
|
||||||
|
background: var(--bg, #f6f8fb); border-color: var(--border);
|
||||||
|
.banner-icon { background: var(--border); color: var(--ink-3); }
|
||||||
|
}
|
||||||
|
.banner-icon {
|
||||||
|
flex: 0 0 48px; width: 48px; height: 48px; border-radius: 12px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.banner-main { flex: 1; min-width: 0; }
|
||||||
|
.banner-title { font-size: 16px; font-weight: 700; color: var(--ink); }
|
||||||
|
.banner-desc { font-size: 13px; color: var(--ink-2); margin-top: 4px; line-height: 1.6; }
|
||||||
|
.reason-text { color: #b91c1c; font-weight: 600; }
|
||||||
|
.banner-badges { display: flex; gap: 8px; margin-top: 8px; }
|
||||||
|
.banner-actions { display: flex; gap: 10px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
/* ===== ② 头部信息卡 ===== */
|
||||||
.detail-header {
|
.detail-header {
|
||||||
display: flex; gap: 24px; align-items: center;
|
display: flex; gap: 24px; align-items: center;
|
||||||
background: var(--surface); border: 1px solid var(--border);
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-lg); padding: 24px 28px; margin-bottom: 16px;
|
border-radius: var(--radius-lg); padding: 24px 28px; margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.header-logo {
|
.header-logo {
|
||||||
flex: 0 0 88px; width: 88px; height: 88px; border-radius: 12px; overflow: hidden;
|
flex: 0 0 88px; width: 88px; height: 88px; border-radius: 14px; overflow: hidden;
|
||||||
background: linear-gradient(135deg, var(--primary), var(--primary-soft, #93c5fd));
|
background: linear-gradient(135deg, var(--primary), var(--primary-soft, #93c5fd));
|
||||||
|
box-shadow: 0 6px 16px -8px var(--primary);
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
}
|
}
|
||||||
.logo-img { width: 100%; height: 100%; }
|
.logo-img { width: 100%; height: 100%; }
|
||||||
@ -306,16 +632,21 @@ onMounted(load)
|
|||||||
background: var(--primary-soft, #e0ecff); color: var(--primary);
|
background: var(--primary-soft, #e0ecff); color: var(--primary);
|
||||||
font-size: 12px; padding: 2px 12px; border-radius: 20px; white-space: nowrap;
|
font-size: 12px; padding: 2px 12px; border-radius: 20px; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.header-meta { display: flex; gap: 20px; flex-wrap: wrap; color: var(--ink-2); font-size: 13px; margin-bottom: 10px; }
|
.header-meta { display: flex; gap: 20px; flex-wrap: wrap; color: var(--ink-2); font-size: 13px; }
|
||||||
.meta-item::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--primary); margin-right: 6px; vertical-align: middle; }
|
.meta-item::before {
|
||||||
|
content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
||||||
|
background: var(--primary); margin-right: 6px; vertical-align: middle;
|
||||||
|
}
|
||||||
.header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
|
.header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
|
||||||
.header-stats { display: flex; gap: 32px; }
|
.header-stats { display: flex; gap: 32px; }
|
||||||
.stat-cell { text-align: center; }
|
.stat-cell { text-align: center; }
|
||||||
.stat-value { font-size: 20px; font-weight: 700; color: var(--ink); }
|
.stat-value { font-size: 18px; font-weight: 700; color: var(--ink); }
|
||||||
.stat-label { font-size: 12px; color: var(--ink-3); margin-top: 4px; }
|
.stat-label { font-size: 12px; color: var(--ink-3); margin-top: 4px; }
|
||||||
|
.header-btns { display: flex; gap: 10px; }
|
||||||
.store-btn { font-weight: 600; }
|
.store-btn { font-weight: 600; }
|
||||||
|
.mono { font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: 13px; }
|
||||||
|
|
||||||
/* ===== Tabs ===== */
|
/* ===== Tab 导航 ===== */
|
||||||
.tab-nav {
|
.tab-nav {
|
||||||
display: flex; gap: 36px; border-bottom: 1px solid var(--border);
|
display: flex; gap: 36px; border-bottom: 1px solid var(--border);
|
||||||
padding: 0 8px; margin-bottom: 20px;
|
padding: 0 8px; margin-bottom: 20px;
|
||||||
@ -327,7 +658,7 @@ onMounted(load)
|
|||||||
.tab-item:hover { color: var(--ink); }
|
.tab-item:hover { color: var(--ink); }
|
||||||
.tab-item.active { color: var(--primary); font-weight: 600; border-bottom-color: var(--primary); }
|
.tab-item.active { color: var(--primary); font-weight: 600; border-bottom-color: var(--primary); }
|
||||||
|
|
||||||
/* ===== Body / cards ===== */
|
/* ===== 卡片通用 ===== */
|
||||||
.tab-body { display: flex; flex-direction: column; gap: 16px; }
|
.tab-body { display: flex; flex-direction: column; gap: 16px; }
|
||||||
.info-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 16px; }
|
.info-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 16px; }
|
||||||
.info-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
.info-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
||||||
@ -336,10 +667,17 @@ onMounted(load)
|
|||||||
border-radius: var(--radius-lg); padding: 22px 24px;
|
border-radius: var(--radius-lg); padding: 22px 24px;
|
||||||
}
|
}
|
||||||
.info-card.full { width: 100%; }
|
.info-card.full { width: 100%; }
|
||||||
.card-title {
|
.card-head {
|
||||||
font-size: 15px; font-weight: 700; color: var(--ink); margin-bottom: 18px;
|
display: flex; align-items: center; gap: 8px; margin-bottom: 18px;
|
||||||
padding-left: 10px; border-left: 3px solid var(--primary);
|
|
||||||
}
|
}
|
||||||
|
.card-head-icon {
|
||||||
|
width: 26px; height: 26px; border-radius: 8px;
|
||||||
|
background: var(--primary-soft, #e0ecff); color: var(--primary);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.card-title-text { font-size: 15px; font-weight: 700; color: var(--ink); }
|
||||||
|
.card-sub { font-size: 12px; color: var(--ink-3); margin-left: auto; }
|
||||||
|
|
||||||
.info-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 48px; }
|
.info-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 48px; }
|
||||||
.info-item { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
.info-item { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
||||||
.info-item.span2 { grid-column: span 2; }
|
.info-item.span2 { grid-column: span 2; }
|
||||||
@ -371,30 +709,50 @@ onMounted(load)
|
|||||||
font-size: 12px; padding: 3px 14px; border-radius: 6px;
|
font-size: 12px; padding: 3px 14px; border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 时间线 ===== */
|
||||||
|
.timeline-card { margin-bottom: 16px; }
|
||||||
|
.audit-timeline { padding-left: 6px; }
|
||||||
|
.tl-content { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.tl-title { font-size: 14px; font-weight: 600; color: var(--ink); }
|
||||||
|
.tl-desc { font-size: 13px; color: var(--ink-2); }
|
||||||
|
.tl-operator { font-size: 12px; color: var(--ink-3); background: var(--bg, #f6f8fb); padding: 2px 10px; border-radius: 12px; }
|
||||||
|
|
||||||
/* ===== 资质 ===== */
|
/* ===== 资质 ===== */
|
||||||
.cert-img { width: 220px; height: 150px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg, #f6f8fb); }
|
.cert-img {
|
||||||
.cert-img.tall { width: 160px; height: 200px; }
|
width: 240px; height: 160px; border-radius: 8px; border: 1px solid var(--border);
|
||||||
|
background: var(--bg, #f6f8fb); cursor: zoom-in;
|
||||||
|
}
|
||||||
|
.cert-img.tall { width: 160px; height: 210px; }
|
||||||
.cert-empty {
|
.cert-empty {
|
||||||
width: 220px; height: 120px; border: 1px dashed var(--border); border-radius: 8px;
|
width: 240px; height: 120px; border: 1px dashed var(--border); border-radius: 8px;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
color: var(--ink-3); font-size: 13px; background: var(--bg, #f6f8fb);
|
color: var(--ink-3); font-size: 13px; background: var(--bg, #f6f8fb);
|
||||||
}
|
}
|
||||||
.cert-empty.small { width: 150px; height: 100px; }
|
.cert-empty.small { width: 160px; height: 110px; }
|
||||||
.idcard-row { display: flex; gap: 24px; margin-bottom: 16px; }
|
.idcard-row { display: flex; gap: 32px; margin-bottom: 16px; }
|
||||||
.idcard-cell { display: flex; flex-direction: column; gap: 8px; }
|
.idcard-cell { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.idcard-img { width: 180px; height: 120px; border-radius: 8px; border: 1px solid var(--border); }
|
.idcard-img { width: 200px; height: 130px; border-radius: 8px; border: 1px solid var(--border); cursor: zoom-in; }
|
||||||
.legal-row { display: flex; gap: 12px; align-items: baseline; }
|
.legal-row { display: flex; gap: 12px; align-items: baseline; }
|
||||||
|
|
||||||
.cert-card {
|
.cert-card {
|
||||||
border: 1px solid var(--border); border-radius: 10px; padding: 16px 18px;
|
border: 1px solid var(--border); border-radius: 10px; padding: 16px 18px;
|
||||||
margin-bottom: 14px; background: var(--bg, #fafbfd);
|
margin-bottom: 14px; background: var(--bg, #fafbfd);
|
||||||
}
|
}
|
||||||
.cert-row { display: flex; gap: 12px; margin-bottom: 6px; }
|
.cert-row { display: flex; gap: 12px; margin-bottom: 6px; align-items: baseline; }
|
||||||
.cert-row .label { min-width: 76px; }
|
.cert-row .label { min-width: 76px; }
|
||||||
.cert-imgs { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 10px; }
|
.cert-imgs { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
|
||||||
.cert-thumb { width: 100px; height: 120px; border-radius: 6px; border: 1px solid var(--border); }
|
.cert-thumb { width: 96px; height: 120px; border-radius: 6px; border: 1px solid var(--border); cursor: zoom-in; }
|
||||||
|
|
||||||
/* ===== 履约 ===== */
|
/* ===== 履约 ===== */
|
||||||
|
.kpi-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
|
||||||
|
.kpi-card {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg); padding: 20px 24px;
|
||||||
|
}
|
||||||
|
.kpi-label { font-size: 12px; color: var(--ink-3); margin-bottom: 10px; }
|
||||||
|
.kpi-value { font-size: 28px; font-weight: 700; color: var(--ink); line-height: 1; }
|
||||||
|
.kpi-value.primary { color: var(--primary); }
|
||||||
|
.kpi-unit { font-size: 14px; color: var(--ink-3); margin-left: 2px; }
|
||||||
.perf-item {
|
.perf-item {
|
||||||
display: flex; align-items: center; gap: 16px; padding: 12px 0;
|
display: flex; align-items: center; gap: 16px; padding: 12px 0;
|
||||||
border-bottom: 1px solid var(--border-light, var(--border));
|
border-bottom: 1px solid var(--border-light, var(--border));
|
||||||
@ -403,9 +761,23 @@ onMounted(load)
|
|||||||
.perf-date { font-size: 13px; color: var(--ink-3); min-width: 140px; }
|
.perf-date { font-size: 13px; color: var(--ink-3); min-width: 140px; }
|
||||||
.perf-content { flex: 1; font-size: 14px; color: var(--ink); }
|
.perf-content { flex: 1; font-size: 14px; color: var(--ink); }
|
||||||
.perf-score { font-size: 15px; font-weight: 700; color: var(--primary); }
|
.perf-score { font-size: 15px; font-weight: 700; color: var(--primary); }
|
||||||
|
.perf-empty { color: var(--ink-3); font-size: 13px; padding: 24px 0; text-align: center; }
|
||||||
|
|
||||||
|
/* ===== 审核弹窗 ===== */
|
||||||
|
.audit-dialog-tip {
|
||||||
|
background: var(--bg, #f6f8fb); border: 1px solid var(--border);
|
||||||
|
border-radius: 8px; padding: 10px 14px; font-size: 13px; color: var(--ink-2);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.pass-tip {
|
||||||
|
font-size: 12px; color: var(--ink-3); line-height: 1.7;
|
||||||
|
background: var(--bg, #f6f8fb); border-radius: 8px; padding: 10px 12px; width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.info-grid { grid-template-columns: 1fr; }
|
.info-grid { grid-template-columns: 1fr; }
|
||||||
.header-stats { display: none; }
|
.header-stats { display: none; }
|
||||||
|
.kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.banner-actions { flex-direction: column; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
125
src/styles/global-ui.scss
Normal file
125
src/styles/global-ui.scss
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/* ============================================================
|
||||||
|
* 后台全局 UI 统一规范 (2026-08-18 建立)
|
||||||
|
* 由 UI 审查驱动, 解决跨页视觉一致性缺口:
|
||||||
|
* 1. 页面骨架统一 (rr-page 默认内边距)
|
||||||
|
* 2. 表格视觉统一 (表头底色/圆角/消除老页面 border 竖线)
|
||||||
|
* 3. 卡片圆角统一
|
||||||
|
* 4. 状态标签/el-tag 暗色主题适配
|
||||||
|
* 设计原则: 只做"兜底默认", 页面 scoped 样式优先覆盖
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
|
/* ---------- 1. 页面骨架: 默认 16px 内边距 (页面未定义时生效) ---------- */
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
|
||||||
|
/* ---------- 2. 表格视觉统一 ---------- */
|
||||||
|
/* 表头底色/文字统一 (el-table 全局) */
|
||||||
|
.el-table {
|
||||||
|
--el-table-header-bg-color: var(--surface-2, #f1f5f9);
|
||||||
|
--el-table-header-text-color: var(--ink-3, #64748b);
|
||||||
|
}
|
||||||
|
.el-table th.el-table__cell {
|
||||||
|
background: var(--surface-2, #f1f5f9) !important; /* !important 覆盖 EP .el-table--border 表头白底 */
|
||||||
|
color: var(--ink-3, #64748b);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
/* 单元格内边距统一 (与 DataTable 规范一致) */
|
||||||
|
.el-table td.el-table__cell { padding: 10px 12px; }
|
||||||
|
|
||||||
|
/* 消除老页面裸 el-table border 属性产生的边框/竖线 → 统一 borderless 视觉 */
|
||||||
|
.el-table--border,
|
||||||
|
.el-table--border .el-table__cell,
|
||||||
|
.el-table--border th.el-table__cell,
|
||||||
|
.el-table--border td.el-table__cell {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.el-table--border .el-table__inner-wrapper::after,
|
||||||
|
.el-table--border .el-table__inner-wrapper::before,
|
||||||
|
.el-table::before,
|
||||||
|
.el-table::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 3. 卡片圆角统一 ---------- */
|
||||||
|
.rr-list-card,
|
||||||
|
.rr-card,
|
||||||
|
.info-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 5. el-tag 统一胶囊外观 (所有页面状态/徽标一致) ---------- */
|
||||||
|
/* EP 变量级联下 --el-tag-border-radius 仍被覆盖为 4px, 直接属性 !important 兜底 */
|
||||||
|
.el-tag {
|
||||||
|
border-radius: 999px !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 6. 通用状态胶囊 (圆点+胶囊, 颜色经 inline style 传 var() 或 --st-* 变量) ---------- */
|
||||||
|
.st-pill {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
font-size: 12px; font-weight: 500; padding: 3px 12px; border-radius: 999px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.st-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||||
|
|
||||||
|
/* ---------- 7. 批量操作栏通用样式 (所有列表页复用) ---------- */
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
/* ---------- 4. 状态色暗色适配 ---------- */
|
||||||
|
/* 状态语义色 token (亮色默认) — 各页面状态标签通过 var() 引用, 主题切换自动适配 */
|
||||||
|
:root,
|
||||||
|
[data-theme="tech-blue"],
|
||||||
|
[data-theme="red"],
|
||||||
|
[data-theme="purple"] {
|
||||||
|
--st-warn-fg: #b45309; --st-warn-bg: #fef3c7;
|
||||||
|
--st-ok-fg: #15803d; --st-ok-bg: #dcfce7;
|
||||||
|
--st-err-fg: #b91c1c; --st-err-bg: #fee2e2;
|
||||||
|
--st-lv1-fg: #92400e; --st-lv1-bg: #fef3c7;
|
||||||
|
--st-lv2-fg: #6d28d9; --st-lv2-bg: #ede9fe;
|
||||||
|
--st-lv3-fg: #c2410c; --st-lv3-bg: #ffedd5;
|
||||||
|
--st-lv4-fg: #475569; --st-lv4-bg: #f1f5f9;
|
||||||
|
--st-lv5-fg: #a16207; --st-lv5-bg: #fefce8;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--st-warn-fg: #fcd34d; --st-warn-bg: rgba(245, 158, 11, 0.16);
|
||||||
|
--st-ok-fg: #6ee7b7; --st-ok-bg: rgba(16, 185, 129, 0.16);
|
||||||
|
--st-err-fg: #fca5a5; --st-err-bg: rgba(239, 68, 68, 0.16);
|
||||||
|
--st-lv1-fg: #fcd34d; --st-lv1-bg: rgba(245, 158, 11, 0.14);
|
||||||
|
--st-lv2-fg: #c4b5fd; --st-lv2-bg: rgba(109, 40, 217, 0.2);
|
||||||
|
--st-lv3-fg: #fdba74; --st-lv3-bg: rgba(194, 65, 12, 0.2);
|
||||||
|
--st-lv4-fg: #cbd5e1; --st-lv4-bg: rgba(100, 116, 139, 0.2);
|
||||||
|
--st-lv5-fg: #fde047; --st-lv5-bg: rgba(161, 98, 7, 0.2);
|
||||||
|
|
||||||
|
/* el-tag 四态: 暗色下深底浅字 */
|
||||||
|
.el-tag--success {
|
||||||
|
--el-tag-bg-color: rgba(16, 185, 129, 0.16);
|
||||||
|
--el-tag-text-color: #6ee7b7;
|
||||||
|
--el-tag-border-color: rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
.el-tag--warning {
|
||||||
|
--el-tag-bg-color: rgba(245, 158, 11, 0.16);
|
||||||
|
--el-tag-text-color: #fcd34d;
|
||||||
|
--el-tag-border-color: rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
.el-tag--danger {
|
||||||
|
--el-tag-bg-color: rgba(239, 68, 68, 0.16);
|
||||||
|
--el-tag-text-color: #fca5a5;
|
||||||
|
--el-tag-border-color: rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
.el-tag--info {
|
||||||
|
--el-tag-bg-color: rgba(148, 163, 184, 0.16);
|
||||||
|
--el-tag-text-color: #cbd5e1;
|
||||||
|
--el-tag-border-color: rgba(148, 163, 184, 0.4);
|
||||||
|
}
|
||||||
|
/* 老式 rr-tag-* 状态类 (浅底深字 -> 深底浅字) */
|
||||||
|
.rr-tag-warn { background: rgba(245, 158, 11, 0.16); color: #fcd34d; }
|
||||||
|
.rr-tag-ok { background: rgba(16, 185, 129, 0.16); color: #6ee7b7; }
|
||||||
|
.rr-tag-err { background: rgba(239, 68, 68, 0.16); color: #fca5a5; }
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user