merge: admin_new P0+P1 全批次 — 83 commits (P0 全量接口探测+17条假数据→真数据+textconfig NPE修复+菜单清理 + P1 12条mock→真view + W9-W17 view 标准化)
This commit is contained in:
commit
0779b31402
5
.gitignore
vendored
5
.gitignore
vendored
@ -8,3 +8,8 @@ auto-imports.d.ts
|
|||||||
components.d.ts
|
components.d.ts
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
|
# 构建产物 (W17.4.6 补)
|
||||||
|
*.tsbuildinfo
|
||||||
|
vite.config.d.ts
|
||||||
|
vite.config.js
|
||||||
|
|||||||
@ -60,6 +60,56 @@ function isActive(path: string): boolean {
|
|||||||
return path === '/' ? route.path === '/' : route.path.startsWith(path)
|
return path === '/' ? route.path === '/' : route.path.startsWith(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面包屑: 从当前 route.path 在菜单树里 DFS 追溯完整父级链
|
||||||
|
* 返回 [一级目录, ..., 末级菜单], 不含"工作台"前缀(template 里单独拼)
|
||||||
|
* 找不到时 fallback 到 route.meta.title
|
||||||
|
*/
|
||||||
|
const breadcrumbItems = computed<string[]>(() => {
|
||||||
|
const currentPath = route.path
|
||||||
|
// 工作台首页不显示中间层级
|
||||||
|
if (currentPath === '/' || currentPath === '/dashboard') return []
|
||||||
|
// DFS 找父级链: 父菜单(有 children)的 path 是第一个子菜单的 path(见
|
||||||
|
// resolveMenuItem), 不能用 path 前缀短路命中父菜单, 必须先递归子树,
|
||||||
|
// 子树没命中才考虑父菜单自身。
|
||||||
|
function findChain(items: MenuItem[], target: string, chain: MenuItem[]): boolean {
|
||||||
|
for (const item of items) {
|
||||||
|
// 叶子节点: path 精确匹配
|
||||||
|
if (!item.children?.length) {
|
||||||
|
if (item.path === target) {
|
||||||
|
chain.push(item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 父节点: 先递归子树
|
||||||
|
chain.push(item)
|
||||||
|
if (findChain(item.children, target, chain)) return true
|
||||||
|
chain.pop()
|
||||||
|
// 子树没命中, 看父节点自身 path 是否是 target 的前缀(详情页场景)
|
||||||
|
if (item.path !== '/' && target.startsWith(item.path)) {
|
||||||
|
chain.push(item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const chain: MenuItem[] = []
|
||||||
|
findChain(menuList.value, currentPath, chain)
|
||||||
|
const metaTitle = route.meta?.title as string | undefined
|
||||||
|
if (chain.length) {
|
||||||
|
const titles: string[] = chain.map(i => i.title)
|
||||||
|
const last = chain[chain.length - 1]
|
||||||
|
if (last.path !== currentPath) {
|
||||||
|
// 详情页: 命中的是父前缀, 追加 meta.title
|
||||||
|
return metaTitle ? [...titles, metaTitle] : titles
|
||||||
|
}
|
||||||
|
return titles
|
||||||
|
}
|
||||||
|
// 菜单树没命中 (如 /inquiry/:id 详情页), 用 meta.title 兜底
|
||||||
|
return metaTitle ? [metaTitle] : []
|
||||||
|
})
|
||||||
|
|
||||||
const PARENT_GROUP: Record<string, string> = {
|
const PARENT_GROUP: Record<string, string> = {
|
||||||
'概览': '概览',
|
'概览': '概览',
|
||||||
'业务': '业务',
|
'业务': '业务',
|
||||||
@ -224,7 +274,8 @@ const badges = ref<Record<string, string>>({})
|
|||||||
async function loadBadges() {
|
async function loadBadges() {
|
||||||
try {
|
try {
|
||||||
const data = await fetch('/sys/openapi/inquiry/list?page=1&limit=1', {
|
const data = await fetch('/sys/openapi/inquiry/list?page=1&limit=1', {
|
||||||
headers: { Authorization: `Bearer ${userStore.token}` }
|
headers: { Authorization: `Bearer ${userStore.token}` },
|
||||||
|
credentials: 'omit'
|
||||||
}).then(r => r.json()).catch(() => null)
|
}).then(r => r.json()).catch(() => null)
|
||||||
if (data?.code === 0) {
|
if (data?.code === 0) {
|
||||||
const total = data.total ?? data.data?.length ?? 0
|
const total = data.total ?? data.data?.length ?? 0
|
||||||
@ -328,7 +379,7 @@ onMounted(loadBadges)
|
|||||||
</div>
|
</div>
|
||||||
<el-breadcrumb separator=">" class="rr-topbar-breadcrumb">
|
<el-breadcrumb separator=">" class="rr-topbar-breadcrumb">
|
||||||
<el-breadcrumb-item :to="{ path: '/' }">工作台</el-breadcrumb-item>
|
<el-breadcrumb-item :to="{ path: '/' }">工作台</el-breadcrumb-item>
|
||||||
<el-breadcrumb-item>{{ route.meta?.title || '' }}</el-breadcrumb-item>
|
<el-breadcrumb-item v-for="item in breadcrumbItems" :key="item">{{ item }}</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
<div class="rr-topbar-right">
|
<div class="rr-topbar-right">
|
||||||
|
|||||||
@ -1,54 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 通用 DataTable 组件
|
* F006 通用 DataTable 组件 (W17 设计规范 v2 整改)
|
||||||
* W3.3:抽取自 InquiryListView + DashboardView 近期询价表
|
*
|
||||||
|
* 按 ui-preview-v2.html 模板:
|
||||||
|
* - 表头 text-transform: uppercase + 左对齐
|
||||||
|
* - 单元格紧凑 padding 12px 16px + 左对齐
|
||||||
|
* - 颜色全用 var(--xxx) 主题化
|
||||||
|
* - 表格外无卡片包裹
|
||||||
*
|
*
|
||||||
* 用法:
|
* 用法:
|
||||||
* <DataTable
|
* <DataTable
|
||||||
* :columns="[
|
* :columns="[
|
||||||
* { prop: 'id', label: '单号', width: 180 },
|
* { prop: 'id', label: '单号', width: 180 },
|
||||||
* { prop: 'customer', label: '客户', minWidth: 180 },
|
|
||||||
* { prop: 'status', label: '状态', width: 110, type: 'status',
|
|
||||||
* statusMap: { pending: '待报价', quoting: '报价中', done: '已成交', cancelled: '已取消' } },
|
|
||||||
* { label: '操作', width: 100, slot: 'action' }
|
* { label: '操作', width: 100, slot: 'action' }
|
||||||
* ]"
|
* ]"
|
||||||
* :data="list"
|
* :data="list" :loading="loading" :total="total" :page="page" :limit="limit"
|
||||||
* :loading="loading"
|
|
||||||
* :total="total"
|
|
||||||
* :page="page"
|
|
||||||
* :limit="limit"
|
|
||||||
* :show-pagination="true"
|
|
||||||
* row-key="id"
|
|
||||||
* @row-click="onRowClick"
|
|
||||||
* @page-change="onPageChange"
|
* @page-change="onPageChange"
|
||||||
* >
|
* >
|
||||||
* <template #action="{ row }">
|
* <template #action="{ row }">
|
||||||
* <el-button text type="primary" size="small" @click.stop="onView(row)">查看</el-button>
|
* <el-button text type="primary" size="small">查看</el-button>
|
||||||
* </template>
|
* </template>
|
||||||
* </DataTable>
|
* </DataTable>
|
||||||
*
|
|
||||||
* 列定义支持:
|
|
||||||
* - prop: 字段名
|
|
||||||
* - label: 列标题
|
|
||||||
* - width / minWidth: 列宽
|
|
||||||
* - type: 'status' 自动渲染 el-tag(需 statusMap)
|
|
||||||
* - statusMap: { value: '中文标签' }
|
|
||||||
* - slot: 自定义插槽名
|
|
||||||
* - formatter: (row) => string 自定义格式化
|
|
||||||
* - align: 对齐方式
|
|
||||||
*/
|
*/
|
||||||
import { computed } from 'vue'
|
import { computed, nextTick, ref, watch, onMounted } from 'vue'
|
||||||
|
|
||||||
interface Column {
|
interface Column {
|
||||||
prop?: string
|
prop?: string
|
||||||
label: string
|
label: string
|
||||||
width?: number | string
|
width?: number | string
|
||||||
minWidth?: number | string
|
minWidth?: number | string
|
||||||
type?: 'status' | 'text'
|
type?: 'status' | 'text' | 'selection'
|
||||||
statusMap?: Record<string, { label: string; type: string }>
|
statusMap?: Record<string, { label: string; type: string }>
|
||||||
slot?: string
|
slot?: string
|
||||||
formatter?: (row: any) => string
|
formatter?: (row: any) => string
|
||||||
align?: 'left' | 'center' | 'right'
|
align?: 'left' | 'center' | 'right'
|
||||||
|
fixed?: 'left' | 'right' | true
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -72,11 +58,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
loading: false,
|
loading: false,
|
||||||
total: 0,
|
total: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 20,
|
limit: 10, // W17.3.5 默认每页 10 条 (按用户原话)
|
||||||
showPagination: true,
|
showPagination: true,
|
||||||
pageSizes: () => [10, 20, 50, 100],
|
pageSizes: () => [10, 20, 50, 100],
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
stripe: true,
|
stripe: false,
|
||||||
borderless: false,
|
borderless: false,
|
||||||
emptyText: '暂无数据',
|
emptyText: '暂无数据',
|
||||||
errorText: '',
|
errorText: '',
|
||||||
@ -87,6 +73,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'row-click', row: any): void
|
(e: 'row-click', row: any): void
|
||||||
(e: 'page-change', page: number): void
|
(e: 'page-change', page: number): void
|
||||||
(e: 'limit-change', limit: number): void
|
(e: 'limit-change', limit: number): void
|
||||||
|
(e: 'selection-change', rows: any[]): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const hasStatusCol = computed(() => props.columns.some(c => c.type === 'status'))
|
const hasStatusCol = computed(() => props.columns.some(c => c.type === 'status'))
|
||||||
@ -97,19 +84,53 @@ function getStatusMeta(col: Column, value: string) {
|
|||||||
return col.statusMap?.[value] || { label: value || '--', type: 'info' }
|
return col.statusMap?.[value] || { label: value || '--', type: 'info' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRowClick(row: any) {
|
function onSelectionChange(rows: any[]) { emit('selection-change', rows) }
|
||||||
emit('row-click', row)
|
function onRowClick(row: any) { emit('row-click', row) }
|
||||||
|
function onPageChange(p: number) { emit('page-change', p) }
|
||||||
|
function onLimitChange(l: number) { emit('limit-change', l) }
|
||||||
|
|
||||||
|
// W17.8.2: 表格自动撑满 (列总宽 < 容器宽时, 给弹性列加 minWidth 填满差额)
|
||||||
|
// W17.8.2 改: 差额加到"中间弹性列" (第一个有 minWidth 且非 fixed 的列), 不是最后一列
|
||||||
|
// 原因: 最后一列通常是"操作列" (fixed: right, width 固定), 加差额会撑大操作列破坏布局
|
||||||
|
// 解决大视口下列少导致右侧大面积空白
|
||||||
|
const tableRef = ref<HTMLElement | null>(null)
|
||||||
|
const lastColMinWidth = ref<number>(0)
|
||||||
|
const flexColIdx = computed(() => {
|
||||||
|
// 优先找第一个有 minWidth 且非 fixed 的数据列 (不是 selection/status)
|
||||||
|
const idx = props.columns.findIndex(c => c.minWidth && !c.fixed && c.type !== 'selection' && c.type !== 'status')
|
||||||
|
return idx >= 0 ? idx : props.columns.length - 1
|
||||||
|
})
|
||||||
|
|
||||||
|
async function autoFillTable() {
|
||||||
|
await nextTick()
|
||||||
|
// 双重 nextTick 等待列渲染
|
||||||
|
await nextTick()
|
||||||
|
const el = tableRef.value
|
||||||
|
if (!el) return
|
||||||
|
const table = el.querySelector('.el-table') as HTMLElement | null
|
||||||
|
if (!table) return
|
||||||
|
const containerW = el.clientWidth
|
||||||
|
const tableW = table.scrollWidth
|
||||||
|
if (tableW >= containerW - 4) {
|
||||||
|
lastColMinWidth.value = 0
|
||||||
|
return
|
||||||
}
|
}
|
||||||
function onPageChange(p: number) {
|
// 差额 + 一些 buffer (避免 border 抖动)
|
||||||
emit('page-change', p)
|
const diff = containerW - tableW + 4
|
||||||
}
|
lastColMinWidth.value = diff
|
||||||
function onLimitChange(l: number) {
|
|
||||||
emit('limit-change', l)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
autoFillTable()
|
||||||
|
window.addEventListener('resize', autoFillTable)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.data, () => autoFillTable(), { flush: 'post' })
|
||||||
|
watch(() => props.columns, () => autoFillTable(), { flush: 'post' })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-data-table" :class="{ borderless }">
|
<div ref="tableRef" class="rr-data-table" :class="{ borderless }">
|
||||||
<el-table
|
<el-table
|
||||||
:data="data"
|
:data="data"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@ -118,41 +139,51 @@ function onLimitChange(l: number) {
|
|||||||
:row-key="rowKey"
|
:row-key="rowKey"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@row-click="onRowClick"
|
@row-click="onRowClick"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
row-class-name="rr-clickable-row"
|
row-class-name="rr-clickable-row"
|
||||||
:header-row-style="{ height: '40px' }"
|
:header-row-style="{ height: '48px' }"
|
||||||
:row-style="{ height: '52px' }"
|
|
||||||
:header-cell-style="{
|
:header-cell-style="{
|
||||||
background: '#F8FAFC',
|
background: 'var(--surface-2)',
|
||||||
color: '#475569',
|
color: 'var(--ink-3)',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
fontSize: '12.5px',
|
fontSize: '12px',
|
||||||
padding: '10px 14px',
|
padding: '14px 16px',
|
||||||
borderBottom: '1px solid #E2E8F0',
|
borderBottom: '1px solid var(--border)',
|
||||||
lineHeight: '18.75px',
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
height: '40px'
|
lineHeight: '20px'
|
||||||
}"
|
}"
|
||||||
:cell-style="{
|
:cell-style="{
|
||||||
padding: '0 14px',
|
padding: '10px 16px',
|
||||||
borderBottom: '1px solid #E2E8F0',
|
borderBottom: '1px solid var(--border)',
|
||||||
fontSize: '13.5px',
|
color: 'var(--ink-2)',
|
||||||
lineHeight: '20.25px',
|
fontSize: '13px',
|
||||||
color: '#0F172A',
|
textAlign: 'left',
|
||||||
height: '52px'
|
verticalAlign: 'middle'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<template v-for="(col, idx) in columns" :key="idx">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-for="(col, idx) in columns"
|
v-if="col.type === 'selection'"
|
||||||
:key="idx"
|
type="selection"
|
||||||
|
:width="col.width || 50"
|
||||||
|
:align="col.align || 'left'"
|
||||||
|
:fixed="col.fixed"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-else
|
||||||
:prop="col.type === 'status' || col.slot ? undefined : col.prop"
|
:prop="col.type === 'status' || col.slot ? undefined : col.prop"
|
||||||
:label="col.label"
|
:label="col.label"
|
||||||
:width="col.width"
|
:width="col.width"
|
||||||
:min-width="col.minWidth"
|
:min-width="idx === flexColIdx && lastColMinWidth > 0 ? Math.max(Number(col.minWidth) || 0, lastColMinWidth) : col.minWidth"
|
||||||
:align="col.align || 'center'"
|
:align="col.align || 'left'"
|
||||||
|
:fixed="col.fixed"
|
||||||
:show-overflow-tooltip="true"
|
:show-overflow-tooltip="true"
|
||||||
>
|
>
|
||||||
<template v-if="col.slot" #default="{ row }">
|
<template v-if="col.slot" #default="{ row, $index }">
|
||||||
<slot :name="col.slot" :row="row" />
|
<slot :name="col.slot" :row="row" :index="$index" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="col.type === 'status'" #default="{ row }">
|
<template v-else-if="col.type === 'status'" #default="{ row }">
|
||||||
<el-tag
|
<el-tag
|
||||||
@ -167,129 +198,110 @@ function onLimitChange(l: number) {
|
|||||||
{{ col.formatter?.(row) }}
|
{{ col.formatter?.(row) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
</template>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="rr-empty">
|
<div class="rr-empty">
|
||||||
<span v-if="error">{{ error }}</span>
|
<span v-if="error" class="rr-empty__error">{{ error || errorText }}</span>
|
||||||
<span v-else>{{ emptyText }}</span>
|
<span v-else>{{ emptyText }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<div v-if="showPager" class="rr-pager">
|
||||||
<div v-if="showPager" class="rr-pagination">
|
<div class="rr-pager__info">
|
||||||
<span class="rr-pagination-total">共 {{ total }} 条记录</span>
|
共 {{ total }} 条
|
||||||
|
</div>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
background
|
|
||||||
layout="prev, pager, next"
|
|
||||||
:total="total"
|
|
||||||
:page-size="limit"
|
|
||||||
:current-page="page"
|
:current-page="page"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:page-size="limit"
|
||||||
:prev-text="'上一页'"
|
:total="total"
|
||||||
:next-text="'下一页'"
|
:page-sizes="pageSizes"
|
||||||
:pager-count="5"
|
:pager-count="5"
|
||||||
|
layout="sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
@current-change="onPageChange"
|
@current-change="onPageChange"
|
||||||
class="rr-pager"
|
@size-change="onLimitChange"
|
||||||
/>
|
/>
|
||||||
<span class="rr-pagination-size">
|
|
||||||
每页
|
|
||||||
<el-select :model-value="limit" size="small" @change="onLimitChange" class="rr-page-size-select">
|
|
||||||
<el-option v-for="s in pageSizes" :key="s" :label="`${s} 行`" :value="s" />
|
|
||||||
</el-select>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-data-table {
|
.rr-data-table {
|
||||||
|
width: 100%;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
// 行 hover 高亮
|
||||||
|
:deep(.el-table__row) {
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
:deep(.el-table__row:hover > td.el-table__cell) {
|
||||||
|
background-color: var(--bg-soft) !important;
|
||||||
|
}
|
||||||
|
// 表格本体圆角配合容器
|
||||||
|
:deep(.el-table) {
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
:deep(.el-table__inner-wrapper) {
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
// 表头单元格强制不换行 (兜底, header-cell-style 的 whiteSpace 在某些列宽挤压下失效)
|
||||||
|
:deep(.el-table__header-wrapper .cell),
|
||||||
|
:deep(.el-table__header th .cell) {
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
// 表头内容容器允许溢出不被截断
|
||||||
|
:deep(.el-table th .cell) {
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
// 去掉 el-table 默认的 border
|
||||||
|
:deep(.el-table--border) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
:deep(.el-table::before), :deep(.el-table::after) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
// borderless 模式:整体无边框
|
||||||
&.borderless {
|
&.borderless {
|
||||||
border: 0;
|
border: none;
|
||||||
border-radius: 0;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.rr-clickable-row { cursor: pointer; }
|
|
||||||
.rr-clickable-row:hover > td { background: var(--bg-soft) !important; }
|
|
||||||
.rr-empty {
|
.rr-empty {
|
||||||
padding: 32px 0;
|
|
||||||
color: var(--ink-3);
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 32px 0;
|
||||||
|
color: var(--ink-3, #94A3B8);
|
||||||
|
font-size: 13px;
|
||||||
|
&__error {
|
||||||
|
color: var(--red, #EF4444);
|
||||||
}
|
}
|
||||||
.rr-pagination {
|
}
|
||||||
padding: 11px 14px;
|
.rr-pager {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-top: 1px solid var(--border);
|
justify-content: space-between;
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 13px; /* W17.3.6 跟内容行一致 (13px) */
|
||||||
color: var(--ink-3);
|
color: var(--ink-3);
|
||||||
font-size: 12.5px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.rr-pagination-total {
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--ink-3);
|
|
||||||
}
|
|
||||||
.rr-pagination-unit {
|
|
||||||
font-size: 12.5px;
|
|
||||||
color: var(--ink-3);
|
|
||||||
margin-left: -4px;
|
|
||||||
}
|
|
||||||
.rr-pagination :deep(.el-pagination) {
|
|
||||||
flex: 1;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
.rr-pagination :deep(.el-pagination .el-pagination__sizes .el-select .el-input__inner) {
|
|
||||||
font-size: 12.5px;
|
|
||||||
}
|
|
||||||
.rr-pagination-size {
|
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
|
||||||
font-size: 12px; color: var(--ink-3);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.rr-page-size-select {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
.rr-page-size-select :deep(.el-input__wrapper) {
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: 0 0 0 1px var(--border) inset;
|
border-top: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
&__info { color: var(--ink-3); }
|
||||||
|
:deep(.el-pagination) {
|
||||||
|
--el-pagination-bg-color: transparent;
|
||||||
|
--el-pagination-font-size: 13px; /* W17.3.6 跟内容行 cell 字号一致 (13px) */
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
:deep(.el-pager li) {
|
||||||
/* 分页器全部按钮 12px(原 14px 太大) + 圆角 + 紧凑间距 */
|
border-radius: 6px;
|
||||||
:deep(.rr-pager .btn-prev),
|
margin: 0 2px;
|
||||||
:deep(.rr-pager .btn-next),
|
|
||||||
:deep(.rr-pager .el-pager li) {
|
|
||||||
font-size: 12px !important;
|
|
||||||
min-width: 28px !important;
|
|
||||||
height: 28px !important;
|
|
||||||
line-height: 28px !important;
|
|
||||||
border-radius: 50% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
}
|
||||||
:deep(.rr-pager .btn-prev),
|
:deep(.el-pagination .btn-prev),
|
||||||
:deep(.rr-pager .btn-next) {
|
:deep(.el-pagination .btn-next) {
|
||||||
padding: 0 8px !important;
|
border-radius: 6px;
|
||||||
border-radius: 14px !important;
|
|
||||||
}
|
}
|
||||||
:deep(.rr-pager .el-pager li) {
|
|
||||||
margin: 0 2px !important;
|
|
||||||
}
|
|
||||||
:deep(.rr-pager) {
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
:deep(.rr-pager .el-pager li.is-active) {
|
|
||||||
border-radius: 50% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 单元格 nowrap 截断(套设计稿 RFQ-20668…) */
|
|
||||||
:deep(.el-table .cell) {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
177
src/components/FormDialog/FormDialog.vue
Normal file
177
src/components/FormDialog/FormDialog.vue
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 通用 FormDialog 组件 (W9 A2)
|
||||||
|
*
|
||||||
|
* Schema 驱动的增改弹窗: 字段定义 → 自动渲染表单 → 校验 → 提交
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* <FormDialog
|
||||||
|
* v-model:visible="editVisible"
|
||||||
|
* :title="editMode === 'add' ? '新增' : '编辑'"
|
||||||
|
* :fields="[
|
||||||
|
* { key: 'name', label: '名称', type: 'text', required: true, maxlength: 50 },
|
||||||
|
* { key: 'status', label: '状态', type: 'radio', options: [
|
||||||
|
* { value: 0, label: '启用' }, { value: 1, label: '禁用' }
|
||||||
|
* ] },
|
||||||
|
* { key: 'remark', label: '备注', type: 'textarea', rows: 3 }
|
||||||
|
* ]"
|
||||||
|
* :model="editForm"
|
||||||
|
* @submit="onSubmitEdit"
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* 字段类型 (W9 阶段支持 5 种):
|
||||||
|
* - text: el-input (单行)
|
||||||
|
* - textarea: el-input type=textarea
|
||||||
|
* - number: el-input-number
|
||||||
|
* - radio: el-radio-group + options
|
||||||
|
* - select: el-select + options
|
||||||
|
*
|
||||||
|
* 通用属性: required / maxlength / min / max / rows / placeholder / disabled
|
||||||
|
*/
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
export interface FormFieldOption {
|
||||||
|
value: any
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormField {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
type?: 'text' | 'textarea' | 'number' | 'radio' | 'select'
|
||||||
|
required?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
default?: any
|
||||||
|
options?: FormFieldOption[]
|
||||||
|
maxlength?: number
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
rows?: number
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
title: string
|
||||||
|
fields: FormField[]
|
||||||
|
model: Record<string, any>
|
||||||
|
width?: string | number
|
||||||
|
submitText?: string
|
||||||
|
cancelText?: string
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 500,
|
||||||
|
submitText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', v: boolean): void
|
||||||
|
(e: 'submit', model: Record<string, any>): void
|
||||||
|
(e: 'cancel'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localVisible = ref(props.visible)
|
||||||
|
watch(() => props.visible, v => localVisible.value = v)
|
||||||
|
function close() { localVisible.value = false; emit('update:visible', false); emit('cancel') }
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
// 必填校验
|
||||||
|
for (const f of props.fields) {
|
||||||
|
if (f.required) {
|
||||||
|
const v = props.model[f.key]
|
||||||
|
if (v === '' || v === null || v === undefined) {
|
||||||
|
return // 让 el-form-item required 提示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit('submit', { ...props.model })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="localVisible"
|
||||||
|
:title="title"
|
||||||
|
:width="typeof width === 'number' ? `${width}px` : width"
|
||||||
|
@close="close"
|
||||||
|
@update:model-value="(v: boolean) => { localVisible = v; emit('update:visible', v) }"
|
||||||
|
>
|
||||||
|
<el-form label-width="80px" :model="model">
|
||||||
|
<el-form-item
|
||||||
|
v-for="f in fields"
|
||||||
|
:key="f.key"
|
||||||
|
:label="f.label"
|
||||||
|
:required="f.required"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-if="f.type === 'text' || !f.type"
|
||||||
|
v-model="model[f.key]"
|
||||||
|
:placeholder="f.placeholder || `请输入${f.label}`"
|
||||||
|
:maxlength="f.maxlength"
|
||||||
|
:show-word-limit="!!f.maxlength"
|
||||||
|
:disabled="f.disabled"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-else-if="f.type === 'textarea'"
|
||||||
|
v-model="model[f.key]"
|
||||||
|
type="textarea"
|
||||||
|
:rows="f.rows || 3"
|
||||||
|
:placeholder="f.placeholder || `请输入${f.label}`"
|
||||||
|
:maxlength="f.maxlength"
|
||||||
|
:show-word-limit="!!f.maxlength"
|
||||||
|
:disabled="f.disabled"
|
||||||
|
/>
|
||||||
|
<el-input-number
|
||||||
|
v-else-if="f.type === 'number'"
|
||||||
|
v-model="model[f.key]"
|
||||||
|
:min="f.min"
|
||||||
|
:max="f.max"
|
||||||
|
:disabled="f.disabled"
|
||||||
|
/>
|
||||||
|
<el-radio-group
|
||||||
|
v-else-if="f.type === 'radio'"
|
||||||
|
v-model="model[f.key]"
|
||||||
|
:disabled="f.disabled"
|
||||||
|
>
|
||||||
|
<el-radio
|
||||||
|
v-for="opt in f.options || []"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<el-select
|
||||||
|
v-else-if="f.type === 'select'"
|
||||||
|
v-model="model[f.key]"
|
||||||
|
:placeholder="f.placeholder || `请选择${f.label}`"
|
||||||
|
:disabled="f.disabled"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="opt in f.options || []"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="close">{{ cancelText }}</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="onSubmit">{{ submitText }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.el-form-item :deep(.el-input),
|
||||||
|
.el-form-item :deep(.el-select),
|
||||||
|
.el-form-item :deep(.el-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
src/components/ListTabs/ListTabs.vue
Normal file
95
src/components/ListTabs/ListTabs.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 通用 ListTabs 组件 (W17 设计规范 v2 整改)
|
||||||
|
*
|
||||||
|
* 按 ui-preview-v2.html 模板: padding 9px 16px + 下划线高亮 + tab-count 圆角 pill
|
||||||
|
*
|
||||||
|
* 全部 [128] 待报价 [23] 进行中 [45] 已完成 [52] 已取消 [8]
|
||||||
|
*/
|
||||||
|
export interface ListTabItem {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: string
|
||||||
|
items: ListTabItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', v: string): void
|
||||||
|
(e: 'change', v: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function onClick(key: string) {
|
||||||
|
emit('update:modelValue', key)
|
||||||
|
emit('change', key)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-list-tabs">
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.key"
|
||||||
|
class="rr-list-tab"
|
||||||
|
:class="{ 'is-active': props.modelValue === item.key }"
|
||||||
|
@click="onClick(item.key)"
|
||||||
|
>
|
||||||
|
<span class="rr-list-tab__label">{{ item.label }}</span>
|
||||||
|
<span v-if="item.count !== undefined" class="rr-list-tab__count">{{ item.count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-list-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.rr-list-tab {
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-3, #64748B);
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: all 0.15s;
|
||||||
|
font-weight: 500;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
|
||||||
|
&:hover { color: var(--ink, #0F172A); }
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: var(--brand, #2563EB);
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom-color: var(--brand, #2563EB);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--ink-3);
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active &__count {
|
||||||
|
background: var(--brand-soft, rgba(37, 99, 235, 0.08));
|
||||||
|
color: var(--brand, #2563EB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
76
src/components/PageHeader/PageHeader.vue
Normal file
76
src/components/PageHeader/PageHeader.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 通用 PageHeader 组件 (W17 设计规范 v2 整改)
|
||||||
|
*
|
||||||
|
* 页面头: 标题 + 副标题 + 操作按钮区 (按 ui-preview-v2.html 模板, 无卡片化)
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* <PageHeader title="订单管理" :sub="`共 ${total} 条`">
|
||||||
|
* <el-button type="primary" @click="onAdd">新增</el-button>
|
||||||
|
* <el-button type="danger">批量删除</el-button>
|
||||||
|
* </PageHeader>
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
sub?: string
|
||||||
|
}
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
sub: ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page-header">
|
||||||
|
<div class="rr-page-header-text">
|
||||||
|
<h2 class="rr-page-title">{{ title }}</h2>
|
||||||
|
<p v-if="sub" class="rr-page-sub">{{ sub }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rr-page-actions">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.rr-page-header-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.rr-page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink, #0F172A);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.rr-page-sub {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--ink-3, #64748B);
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.rr-page-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
:deep(.el-button) {
|
||||||
|
padding: 9px 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: var(--radius, 6px);
|
||||||
|
}
|
||||||
|
:deep(.el-button--primary) {
|
||||||
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.15);
|
||||||
|
}
|
||||||
|
:deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
274
src/components/SearchBar/SearchBar.vue
Normal file
274
src/components/SearchBar/SearchBar.vue
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 通用 SearchBar 组件 (W17 设计规范 v2 整改)
|
||||||
|
*
|
||||||
|
* 按 ui-preview-v2.html 模板: 紧凑布局 + 无 label + 同行末尾 chip + 简洁按钮
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* <SearchBar
|
||||||
|
* v-model:filters="filters"
|
||||||
|
* :fields="[
|
||||||
|
* { key: 'name', label: '名称', type: 'text', placeholder: '请输入' },
|
||||||
|
* { key: 'status', label: '状态', type: 'select', options: [...] }
|
||||||
|
* ]"
|
||||||
|
* @search="onSearch"
|
||||||
|
* @reset="onReset"
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
export interface SearchFieldOption {
|
||||||
|
value: any
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchField {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
type?: 'text' | 'select' | 'date' | 'daterange' | 'number'
|
||||||
|
placeholder?: string
|
||||||
|
options?: SearchFieldOption[]
|
||||||
|
width?: number | string
|
||||||
|
clearable?: boolean
|
||||||
|
showLabel?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
filters: Record<string, any>
|
||||||
|
fields: SearchField[]
|
||||||
|
showReset?: boolean
|
||||||
|
showChips?: boolean
|
||||||
|
showLabel?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
showReset: true,
|
||||||
|
showChips: true,
|
||||||
|
showLabel: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:filters', filters: Record<string, any>): void
|
||||||
|
(e: 'search'): void
|
||||||
|
(e: 'reset'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localFilters = computed({
|
||||||
|
get: () => props.filters,
|
||||||
|
set: (v) => emit('update:filters', v)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSearch() { emit('search') }
|
||||||
|
function onReset() {
|
||||||
|
for (const f of props.fields) {
|
||||||
|
if (f.type === 'daterange') localFilters.value[f.key] = []
|
||||||
|
else if (f.type === 'number') localFilters.value[f.key] = undefined
|
||||||
|
else localFilters.value[f.key] = ''
|
||||||
|
}
|
||||||
|
emit('reset')
|
||||||
|
}
|
||||||
|
function onEnter() { onSearch() }
|
||||||
|
function onClearField(key: string) {
|
||||||
|
const f = props.fields.find(it => it.key === key)
|
||||||
|
if (!f) return
|
||||||
|
if (f.type === 'daterange') localFilters.value[key] = []
|
||||||
|
else if (f.type === 'number') localFilters.value[key] = undefined
|
||||||
|
else localFilters.value[key] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeChips = computed(() => {
|
||||||
|
return props.fields
|
||||||
|
.map(f => {
|
||||||
|
const v = props.filters[f.key]
|
||||||
|
let display: string | null = null
|
||||||
|
if (f.type === 'daterange') {
|
||||||
|
if (Array.isArray(v) && v.length === 2) display = `${v[0]} ~ ${v[1]}`
|
||||||
|
} else if (f.type === 'select' && v !== '' && v !== undefined && v !== null) {
|
||||||
|
const opt = f.options?.find(o => String(o.value) === String(v))
|
||||||
|
display = opt ? opt.label : String(v)
|
||||||
|
} else if (v !== '' && v !== undefined && v !== null) {
|
||||||
|
display = String(v)
|
||||||
|
}
|
||||||
|
if (display) {
|
||||||
|
return { key: f.key, label: `${f.label}: ${display}` }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter(Boolean) as { key: string; label: string }[]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-search-bar">
|
||||||
|
<div class="rr-search-bar__row">
|
||||||
|
<div class="rr-search-bar__field" v-for="f in fields" :key="f.key">
|
||||||
|
<span v-if="showLabel || f.showLabel" class="rr-search-bar__label">{{ f.label }}</span>
|
||||||
|
<el-input
|
||||||
|
v-if="f.type === 'text' || !f.type"
|
||||||
|
v-model="localFilters[f.key]"
|
||||||
|
:placeholder="f.placeholder || `请输入${f.label}`"
|
||||||
|
:clearable="f.clearable !== false"
|
||||||
|
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
|
||||||
|
@keyup.enter="onEnter"
|
||||||
|
/>
|
||||||
|
<el-select
|
||||||
|
v-else-if="f.type === 'select'"
|
||||||
|
v-model="localFilters[f.key]"
|
||||||
|
:placeholder="f.placeholder || `请选择${f.label}`"
|
||||||
|
:clearable="f.clearable !== false"
|
||||||
|
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '160px')) }"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="opt in f.options || []"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker
|
||||||
|
v-else-if="f.type === 'date'"
|
||||||
|
v-model="localFilters[f.key]"
|
||||||
|
type="date"
|
||||||
|
:placeholder="f.placeholder || `请选择${f.label}`"
|
||||||
|
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
<el-date-picker
|
||||||
|
v-else-if="f.type === 'daterange'"
|
||||||
|
v-model="localFilters[f.key]"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="~"
|
||||||
|
:start-placeholder="f.placeholder || '开始日期'"
|
||||||
|
:end-placeholder="'结束日期'"
|
||||||
|
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '260px')) }"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
<el-input-number
|
||||||
|
v-else-if="f.type === 'number'"
|
||||||
|
v-model="localFilters[f.key]"
|
||||||
|
:placeholder="f.placeholder || `请输入${f.label}`"
|
||||||
|
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rr-search-bar__chips" v-if="showChips && activeChips.length">
|
||||||
|
<div
|
||||||
|
v-for="c in activeChips"
|
||||||
|
:key="c.key"
|
||||||
|
class="rr-search-bar__chip"
|
||||||
|
>
|
||||||
|
{{ c.label }}
|
||||||
|
<span class="rr-search-bar__chip-close" @click="onClearField(c.key)">×</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rr-search-bar__actions">
|
||||||
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
|
<el-button v-if="showReset" @click="onReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
/* === 统一 input/select 圆角 (按 ui-preview-v2.html 模板) === */
|
||||||
|
:deep(.el-input__wrapper),
|
||||||
|
:deep(.el-select__wrapper) {
|
||||||
|
border-radius: var(--radius, 8px);
|
||||||
|
box-shadow: 0 0 0 1px var(--border) inset;
|
||||||
|
transition: box-shadow 0.15s, border-color 0.15s;
|
||||||
|
min-height: 34px;
|
||||||
|
}
|
||||||
|
:deep(.el-input__wrapper:hover),
|
||||||
|
:deep(.el-select__wrapper:hover) {
|
||||||
|
box-shadow: 0 0 0 1px var(--ink-3, #94A3B8) inset;
|
||||||
|
}
|
||||||
|
:deep(.el-input__wrapper.is-focus),
|
||||||
|
:deep(.el-select__wrapper.is-focused) {
|
||||||
|
box-shadow: 0 0 0 1px var(--brand, #2563EB) inset, 0 0 0 3px var(--brand-glow, rgba(37, 99, 235, 0.18));
|
||||||
|
}
|
||||||
|
/* === el-select 宽度兜底 (Vue :style 没生效时也保证最小宽度) === */
|
||||||
|
:deep(.el-select) {
|
||||||
|
min-width: 140px;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
:deep(.el-select .el-select__wrapper) {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
:deep(.el-select .el-select__wrapper .el-select__selection) {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
/* === 下拉弹层圆角 (跟 input 一致) === */
|
||||||
|
:deep(.el-select-dropdown),
|
||||||
|
:deep(.el-select__popper),
|
||||||
|
:deep(.el-popper.is-light) {
|
||||||
|
border-radius: var(--radius, 8px) !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
:deep(.el-select-dropdown__item) {
|
||||||
|
border-radius: var(--radius-sm, 6px);
|
||||||
|
margin: 2px 4px;
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
:deep(.el-select-dropdown__item.is-hovering) {
|
||||||
|
background: var(--brand-soft, rgba(37, 99, 235, 0.08));
|
||||||
|
color: var(--brand, #2563EB);
|
||||||
|
}
|
||||||
|
:deep(.el-select-dropdown__item.is-selected) {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand, #2563EB);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rr-search-bar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
&__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
&__field {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&__label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-2, #334155);
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
&__chips {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
&__chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: var(--brand-soft, rgba(37, 99, 235, 0.08));
|
||||||
|
color: var(--brand, #2563EB);
|
||||||
|
border-radius: var(--radius-pill, 9999px);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
&-close {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
&:hover { opacity: 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
:deep(.el-button) {
|
||||||
|
padding: 7px 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: var(--radius, 8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
168
src/components/TableActions/TableActions.vue
Normal file
168
src/components/TableActions/TableActions.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 通用表格操作列组件 (W17.6 主次分离, W17.6.1 按数量自动收纳)
|
||||||
|
*
|
||||||
|
* 按钮数量决定显示方式 (规则符合直觉, 无需人为划分主次):
|
||||||
|
* - 过滤 when 后 ≤3 个: 全部显示为文字按钮, 不显示 ⋯
|
||||||
|
* - 过滤 when 后 ≥4 个: 前 visibleCount 个显示为文字按钮, 其余收进 ⋯ 下拉
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* <TableActions
|
||||||
|
* :row="row"
|
||||||
|
* :actions="[
|
||||||
|
* { label: '详情', type: 'primary', onClick: onView },
|
||||||
|
* { label: '审核', type: 'primary', when: r => r.status === 1, onClick: onAudit },
|
||||||
|
* { label: r => r.onShelf === 1 ? '下架' : '上架', type: r => r.onShelf === 1 ? 'warning' : 'success', onClick: onToggle },
|
||||||
|
* { label: '改分类', type: 'primary', onClick: onCategory },
|
||||||
|
* { label: '删除', type: 'danger', divided: true, confirm: '确认删除?', onClick: onDelete }
|
||||||
|
* ]"
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* ActionItem:
|
||||||
|
* label string | (row) => string 文字 (支持动态)
|
||||||
|
* type string | (row) => string 类型 primary/success/warning/danger (支持动态)
|
||||||
|
* when (row) => boolean 条件渲染, 不满足则不显示 (替代 v-if)
|
||||||
|
* divided boolean 菜单项前加分隔线 (危险操作前用)
|
||||||
|
* confirm boolean | string true 用默认提示, string 用自定义提示
|
||||||
|
* onClick (row) => void 点击处理
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* row 当前行数据 (透传给 label/type/when/onClick)
|
||||||
|
* actions 操作列表 (按顺序, 前面的是主操作)
|
||||||
|
* visibleCount 常驻文字按钮数, 默认 2 (仅在按钮 >3 时生效)
|
||||||
|
*/
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
|
import { MoreFilled } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
type ActionType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
||||||
|
|
||||||
|
interface ActionItem {
|
||||||
|
label: string | ((row: any) => string)
|
||||||
|
type?: ActionType | ((row: any) => ActionType)
|
||||||
|
when?: (row: any) => boolean
|
||||||
|
divided?: boolean
|
||||||
|
confirm?: boolean | string
|
||||||
|
onClick: (row: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
row: any
|
||||||
|
actions: ActionItem[]
|
||||||
|
/** 常驻文字按钮数, 仅在按钮 >3 时生效 (默认 2) */
|
||||||
|
visibleCount?: number
|
||||||
|
}>(), {
|
||||||
|
visibleCount: 2
|
||||||
|
})
|
||||||
|
|
||||||
|
// 解析 label (string 或函数)
|
||||||
|
function resolveLabel(item: ActionItem): string {
|
||||||
|
return typeof item.label === 'function' ? item.label(props.row) : item.label
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 type (string 或函数)
|
||||||
|
function resolveType(item: ActionItem): ActionType {
|
||||||
|
const t = typeof item.type === 'function' ? item.type(props.row) : item.type
|
||||||
|
return t || 'primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤 when 条件后的有效项
|
||||||
|
const visibleActions = computed<ActionItem[]>(() =>
|
||||||
|
props.actions.filter(item => !item.when || item.when(props.row))
|
||||||
|
)
|
||||||
|
|
||||||
|
// 拆分结果: 前 N 个常驻 + 其余收纳 (≤3 全常驻, >3 前 visibleCount 常驻)
|
||||||
|
const layout = computed(() => {
|
||||||
|
const all = visibleActions.value
|
||||||
|
if (all.length > 3) {
|
||||||
|
return { primary: all.slice(0, props.visibleCount), more: all.slice(props.visibleCount) }
|
||||||
|
}
|
||||||
|
return { primary: all, more: [] as ActionItem[] }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 执行操作 (处理二次确认)
|
||||||
|
async function runAction(item: ActionItem) {
|
||||||
|
if (item.confirm) {
|
||||||
|
const msg = typeof item.confirm === 'string' ? item.confirm : `确认${resolveLabel(item)}?`
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(msg, '操作确认', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' })
|
||||||
|
} catch {
|
||||||
|
return // 用户取消
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
item.onClick(props.row)
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-table-actions">
|
||||||
|
<!-- 常驻文字按钮 -->
|
||||||
|
<el-button
|
||||||
|
v-for="(item, i) in layout.primary"
|
||||||
|
:key="`p${i}`"
|
||||||
|
text
|
||||||
|
:type="resolveType(item) as any"
|
||||||
|
size="small"
|
||||||
|
@click="runAction(item)"
|
||||||
|
>{{ resolveLabel(item) }}</el-button>
|
||||||
|
|
||||||
|
<!-- 收纳下拉 (仅在按钮 >3 时渲染) -->
|
||||||
|
<el-dropdown
|
||||||
|
v-if="layout.more.length"
|
||||||
|
trigger="click"
|
||||||
|
@command="(cmd: number) => runAction(layout.more[cmd])"
|
||||||
|
>
|
||||||
|
<span class="rr-more-trigger">
|
||||||
|
<el-icon><MoreFilled /></el-icon>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="(item, i) in layout.more"
|
||||||
|
:key="`m${i}`"
|
||||||
|
:command="i"
|
||||||
|
:divided="item.divided"
|
||||||
|
:style="{ color: resolveType(item) === 'danger' ? 'var(--el-color-danger)' : undefined }"
|
||||||
|
>{{ resolveLabel(item) }}</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-table-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
// 收紧 el-button text 的内边距, 默认 padding: 8px 15px 太松散
|
||||||
|
:deep(.el-button.is-text) {
|
||||||
|
padding: 0 4px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-more-trigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--ink-2, #475569);
|
||||||
|
font-size: 15px;
|
||||||
|
margin-left: 2px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-fill-color-light, #F4F4F5);
|
||||||
|
color: var(--el-color-primary, #2563EB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -47,14 +47,15 @@ export interface MenuLabel {
|
|||||||
export const MENU_LABELS: MenuLabel[] = [
|
export const MENU_LABELS: MenuLabel[] = [
|
||||||
// ===== 顶部菜单(原老 vue 一级) =====
|
// ===== 顶部菜单(原老 vue 一级) =====
|
||||||
{ url: 'statistics/index', label: '首页', icon: 'HomeFilled', path: '/dashboard', sort: 1 },
|
{ url: 'statistics/index', label: '首页', icon: 'HomeFilled', path: '/dashboard', sort: 1 },
|
||||||
{ url: 'order/order', label: '订单管理', icon: 'Document', path: '/order', sort: 3 },
|
|
||||||
|
|
||||||
// ===== 用户模块 =====
|
// ===== 用户模块 =====
|
||||||
{ url: 'user/user', label: '用户列表', icon: 'User', path: '/user/user', sort: 100 },
|
{ url: 'user/user', label: '用户列表', icon: 'User', path: '/user/user', sort: 100 },
|
||||||
{ url: 'user/enterprise/enterprise', label: '企业列表', icon: 'OfficeBuilding', path: '/user/enterprise', sort: 101 },
|
{ url: 'user/enterprise/enterprise', label: '企业列表', icon: 'OfficeBuilding', path: '/user/enterprise', sort: 101 },
|
||||||
{ url: 'user/enterprise/enterprise-rating', label: '评级', icon: 'Star', path: '/user/enterprise/rating', sort: 102 },
|
{ url: 'user/enterprise/enterprise-rating', label: '评级', icon: 'Star', path: '/user/enterprise/rating', sort: 102 },
|
||||||
{ url: 'user/enterprise/enterprise-level', label: '企业等级管理', icon: 'Medal', path: '/user/enterprise/level', sort: 103 },
|
{ url: 'user/enterprise/enterprise-level', label: '企业等级管理', icon: 'Medal', path: '/user/enterprise/level', sort: 103 }, // 已接真 view W16
|
||||||
{ url: 'user/enterprise/enterprise-score', label: '评分', icon: 'DataAnalysis', path: '/user/enterprise/score', sort: 104 },
|
{ url: 'user/enterprise/enterprise-score', label: '评分', icon: 'DataAnalysis', path: '/user/enterprise/score', sort: 104 },
|
||||||
|
// F002-W18 (2026-07-14) 新增: 企业登记管理
|
||||||
|
{ url: 'user/enterprise/merchant/register', label: '企业登记管理', icon: 'Postcard', path: '/user/enterprise/merchant/register', sort: 105 },
|
||||||
{ url: 'user/finance/recharge', label: '充值管理', icon: 'Wallet', path: '/user/finance/recharge', sort: 110 },
|
{ url: 'user/finance/recharge', label: '充值管理', icon: 'Wallet', path: '/user/finance/recharge', sort: 110 },
|
||||||
{ url: 'user/finance/recharge-tx', label: '提现管理', icon: 'CreditCard', path: '/user/finance/recharge-tx', sort: 111 },
|
{ url: 'user/finance/recharge-tx', label: '提现管理', icon: 'CreditCard', path: '/user/finance/recharge-tx', sort: 111 },
|
||||||
{ url: 'user/finance/revenue-sharing', label: '分账记录管理', icon: 'Share', path: '/user/finance/revenue', sort: 112 },
|
{ url: 'user/finance/revenue-sharing', label: '分账记录管理', icon: 'Share', path: '/user/finance/revenue', sort: 112 },
|
||||||
@ -72,14 +73,21 @@ export const MENU_LABELS: MenuLabel[] = [
|
|||||||
{ url: 'order/ordergood-commission', label: '实物订单分佣统计', icon: 'Coin', path: '/order/ordergood/commission', sort: 220 },
|
{ url: 'order/ordergood-commission', label: '实物订单分佣统计', icon: 'Coin', path: '/order/ordergood/commission', sort: 220 },
|
||||||
{ url: 'order/serviceorder-commission', label: '服务订单分佣统计', icon: 'Coin', path: '/order/serviceorder/commission', sort: 221 },
|
{ url: 'order/serviceorder-commission', label: '服务订单分佣统计', icon: 'Coin', path: '/order/serviceorder/commission', sort: 221 },
|
||||||
{ url: 'order/orderbase-price-analysis', label: '基础价格分析表', icon: 'TrendCharts', path: '/order/base-price-analysis', sort: 230 },
|
{ url: 'order/orderbase-price-analysis', label: '基础价格分析表', icon: 'TrendCharts', path: '/order/base-price-analysis', sort: 230 },
|
||||||
|
{ url: 'order/orderpay', label: '订单支付回调', icon: 'Wallet', path: '/order/orderpay', sort: 240 },
|
||||||
|
{ url: 'order/refund', label: '订单退款管理', icon: 'Refresh', path: '/order/refund', sort: 250 },
|
||||||
|
{ url: 'order/aftersale', label: '售后订单', icon: 'Service', path: '/order/aftersale', sort: 260 },
|
||||||
|
|
||||||
// ===== 供应商模块 =====
|
// ===== 供应商模块 =====
|
||||||
{ url: 'supplier/good', label: '实物商品', icon: 'Box', path: '/supplier/good', sort: 300 },
|
{ url: 'supplier/good', label: '实物商品', icon: 'Box', path: '/supplier/good', sort: 300 },
|
||||||
{ url: 'supplier/servicegood', label: '服务商品(→实物商品)', icon: 'Box', path: '/supplier/good', sort: 301 },
|
{ url: 'supplier/servicegood', label: '服务商品', icon: 'Box', path: '/supplier/servicegood', sort: 301 },
|
||||||
{ url: 'supplier/activityzone', label: '活动专区', icon: 'Promotion', path: '/supplier/activity', sort: 302 },
|
{ url: 'supplier/activityzone', label: '活动专区', icon: 'Promotion', path: '/supplier/activity', sort: 302 },
|
||||||
{ url: 'supplier/coupon', label: '优惠券', icon: 'Ticket', path: '/supplier/coupon', sort: 303 },
|
{ url: 'supplier/coupon', label: '优惠券', icon: 'Ticket', path: '/supplier/coupon', sort: 303 },
|
||||||
{ url: 'supplier/promoter', label: '推广返佣', icon: 'UserFilled', path: '/supplier/promoter', sort: 304 },
|
{ url: 'supplier/promoter', label: '推广员', icon: 'UserFilled', path: '/supplier/promoter', sort: 304 },
|
||||||
{ url: 'supplier/inquiry/inquiry', label: '询价列表', icon: 'ChatLineSquare', path: '/supplier/inquiry', sort: 305 },
|
{ url: 'supplier/inquiry/inquiry', label: '询价列表', icon: 'ChatLineSquare', path: '/supplier/inquiry', sort: 305 },
|
||||||
|
{ url: 'supplier/ordercomment', label: '订单评价', icon: 'Star', path: '/supplier/ordercomment', sort: 306 },
|
||||||
|
{ url: 'supplier/historylog', label: '浏览足迹', icon: 'View', path: '/supplier/historylog', sort: 307 },
|
||||||
|
{ url: 'supplier/logistics', label: '物流方案', icon: 'Van', path: '/supplier/logistics', sort: 308 },
|
||||||
|
{ url: 'supplier/coupon-scope', label: '优惠券适用范围',icon: 'Filter', path: '/supplier/coupon-scope',sort: 309 },
|
||||||
|
|
||||||
// ===== 系统模块 =====
|
// ===== 系统模块 =====
|
||||||
{ url: 'sys/user', label: '用户管理[后台用户]', icon: 'UserFilled', path: '/sys/user', sort: 400 },
|
{ url: 'sys/user', label: '用户管理[后台用户]', icon: 'UserFilled', path: '/sys/user', sort: 400 },
|
||||||
@ -108,7 +116,7 @@ export const MENU_LABELS: MenuLabel[] = [
|
|||||||
{ url: 'other/help', label: '帮助中心', icon: 'QuestionFilled', path: '/other/help', sort: 701 },
|
{ url: 'other/help', label: '帮助中心', icon: 'QuestionFilled', path: '/other/help', sort: 701 },
|
||||||
{ url: 'other/operateguide', label: '内容列表', icon: 'Reading', path: '/other/guide', sort: 702 },
|
{ url: 'other/operateguide', label: '内容列表', icon: 'Reading', path: '/other/guide', sort: 702 },
|
||||||
{ url: 'other/industry', label: '行业', icon: 'OfficeBuilding', path: '/other/industry', sort: 703 },
|
{ url: 'other/industry', label: '行业', icon: 'OfficeBuilding', path: '/other/industry', sort: 703 },
|
||||||
{ url: 'other/label', label: '标签管理', icon: 'PriceTag', path: '/other/label', sort: 704 },
|
// other/label 已隐藏 (后端无表无 controller)
|
||||||
{ url: 'other/categoryGood', label: '平台分类', icon: 'Menu', path: '/other/category/good', sort: 705 },
|
{ url: 'other/categoryGood', label: '平台分类', icon: 'Menu', path: '/other/category/good', sort: 705 },
|
||||||
{ url: 'other/categoryZX', label: '资讯分类', icon: 'Menu', path: '/other/category/zx', sort: 707 },
|
{ url: 'other/categoryZX', label: '资讯分类', icon: 'Menu', path: '/other/category/zx', sort: 707 },
|
||||||
{ url: 'other/goodunit', label: '商品单位', icon: 'Box', path: '/other/goodunit', sort: 708 },
|
{ url: 'other/goodunit', label: '商品单位', icon: 'Box', path: '/other/goodunit', sort: 708 },
|
||||||
@ -121,12 +129,7 @@ export const MENU_LABELS: MenuLabel[] = [
|
|||||||
{ url: 'message/sms', label: '短信配置', icon: 'ChatDotRound', path: '/message/sms', sort: 800 },
|
{ url: 'message/sms', label: '短信配置', icon: 'ChatDotRound', path: '/message/sms', sort: 800 },
|
||||||
{ url: 'job/schedule', label: '定时任务', icon: 'Timer', path: '/job/schedule', sort: 801 },
|
{ url: 'job/schedule', label: '定时任务', icon: 'Timer', path: '/job/schedule', sort: 801 },
|
||||||
|
|
||||||
// ===== 开发工具(devtools) =====
|
// ===== 开发工具(devtools) — 已隐藏 (客户不需要) =====
|
||||||
{ url: 'devtools/datasource', label: '数据源配置', icon: 'Coin', path: '/devtools/datasource', sort: 901 },
|
|
||||||
{ url: 'devtools/param', label: '参数配置(开发)', icon: 'SetUp', path: '/devtools/param', sort: 902 },
|
|
||||||
{ url: 'devtools/baseclass', label: '基类配置', icon: 'Files', path: '/devtools/baseclass', sort: 903 },
|
|
||||||
{ url: 'devtools/template', label: '模板配置', icon: 'Tickets', path: '/devtools/template', sort: 904 },
|
|
||||||
{ url: 'devtools/fieldtype', label: '字段配置', icon: 'CollectionTag', path: '/devtools/fieldtype', sort: 905 },
|
|
||||||
|
|
||||||
// ===== F002 智能体集成 =====
|
// ===== F002 智能体集成 =====
|
||||||
{ url: 'openapi/app', label: '应用列表', icon: 'Cellphone', path: '/openapi/app', sort: 1000 },
|
{ url: 'openapi/app', label: '应用列表', icon: 'Cellphone', path: '/openapi/app', sort: 1000 },
|
||||||
|
|||||||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@ -5,3 +5,4 @@ declare module '*.vue' {
|
|||||||
const component: DefineComponent<{}, {}, any>
|
const component: DefineComponent<{}, {}, any>
|
||||||
export default component
|
export default component
|
||||||
}
|
}
|
||||||
|
declare module 'element-plus/dist/locale/zh-cn.mjs';
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import { createPinia } from 'pinia'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
|
|
||||||
// 主题系统(必须在 App 之前 import)
|
// 主题系统(必须在 App 之前 import)
|
||||||
import './theme/tokens.scss'
|
import './theme/tokens.scss'
|
||||||
import './theme/themes.scss'
|
import './theme/themes.scss'
|
||||||
|
import './styles/global-popper.scss'
|
||||||
import { loadTheme } from './theme/presets'
|
import { loadTheme } from './theme/presets'
|
||||||
|
|
||||||
// 启动时恢复用户主题
|
// 启动时恢复用户主题
|
||||||
@ -16,5 +18,5 @@ document.body.setAttribute('data-theme', loadTheme())
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus, { locale: zhCn })
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 意见反馈 - W6 P0 完整化
|
* F006 意见反馈 - W10 A1 标准化
|
||||||
*
|
*
|
||||||
* 沿用老 vue api/feedback.vue 137 行:
|
* 沿用老 vue api/feedback.vue 137 行:
|
||||||
* - 搜索: 用户昵称
|
* - 搜索: 用户昵称
|
||||||
@ -8,11 +8,15 @@
|
|||||||
* - 操作: 预览 / 删除
|
* - 操作: 预览 / 删除
|
||||||
* - 预览弹窗展示完整反馈内容 (含图片列表)
|
* - 预览弹窗展示完整反馈内容 (含图片列表)
|
||||||
* - /api/feedback 后端 404 → FALLBACK mock
|
* - /api/feedback 后端 404 → FALLBACK mock
|
||||||
|
*
|
||||||
|
* W10 A1: 套 PageHeader/SearchBar 标准化模板 (无 STATUS_MAP, 无增改弹窗)
|
||||||
*/
|
*/
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Feedback {
|
interface Feedback {
|
||||||
id: string
|
id: string
|
||||||
@ -27,6 +31,7 @@ interface Feedback {
|
|||||||
raw: any
|
raw: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 端点 404 降级 mock ==========
|
||||||
const FALLBACK: Feedback[] = [
|
const FALLBACK: Feedback[] = [
|
||||||
{ id: 'F001', userId: 'U10086', nickName: '张经理', phone: '13800138001', title: '订单延迟发货', content: '我们于6月28日下单采购一批钢材, 至今未发货, 影响项目进度, 请尽快处理。', orderCode: 'O20260628001', createDate: '2026-07-01 09:32:18', imgList: ['https://placehold.co/120x120/2563EB/fff?text=IMG1', 'https://placehold.co/120x120/2563EB/fff?text=IMG2'], raw: {} },
|
{ id: 'F001', userId: 'U10086', nickName: '张经理', phone: '13800138001', title: '订单延迟发货', content: '我们于6月28日下单采购一批钢材, 至今未发货, 影响项目进度, 请尽快处理。', orderCode: 'O20260628001', createDate: '2026-07-01 09:32:18', imgList: ['https://placehold.co/120x120/2563EB/fff?text=IMG1', 'https://placehold.co/120x120/2563EB/fff?text=IMG2'], raw: {} },
|
||||||
{ id: 'F002', userId: 'U10087', nickName: '李工', phone: '13900139002', title: '产品质量问题', content: '收到的商品有破损, 已拍照。附件图1是外包装破损, 图2是商品本身。', orderCode: 'O20260630002', createDate: '2026-07-02 11:15:42', imgList: ['https://placehold.co/120x120/DC2626/fff?text=BAD1'], raw: {} },
|
{ id: 'F002', userId: 'U10087', nickName: '李工', phone: '13900139002', title: '产品质量问题', content: '收到的商品有破损, 已拍照。附件图1是外包装破损, 图2是商品本身。', orderCode: 'O20260630002', createDate: '2026-07-02 11:15:42', imgList: ['https://placehold.co/120x120/DC2626/fff?text=BAD1'], raw: {} },
|
||||||
@ -36,26 +41,27 @@ const FALLBACK: Feedback[] = [
|
|||||||
{ id: 'F006', userId: 'U10091', nickName: '孙技术', phone: '13400134006', title: '功能建议: 自定义审批流', content: '建议在询价单模块增加自定义审批流, 当前固定流程不够灵活。', orderCode: '', createDate: '2026-07-04 09:10:00', imgList: [], raw: {} }
|
{ id: 'F006', userId: 'U10091', nickName: '孙技术', phone: '13400134006', title: '功能建议: 自定义审批流', content: '建议在询价单模块增加自定义审批流, 当前固定流程不够灵活。', orderCode: '', createDate: '2026-07-04 09:10:00', imgList: [], raw: {} }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ========== 标准状态 ==========
|
||||||
const list = ref<Feedback[]>([])
|
const list = ref<Feedback[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const limit = ref(20)
|
const limit = ref(20)
|
||||||
const filters = reactive({ nickName: '' })
|
const filters = reactive<{ nickName: string }>({ nickName: '' })
|
||||||
|
|
||||||
|
// ========== 弹窗状态 ==========
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
const previewData = ref<Feedback | null>(null)
|
const previewData = ref<Feedback | null>(null)
|
||||||
|
|
||||||
|
// ========== 加载逻辑 ==========
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await new Promise(r => setTimeout(r, 200))
|
const resp: any = await httpGet('/other/feedback/page', { page: page.value, limit: limit.value })
|
||||||
let rows = [...FALLBACK]
|
list.value = resp?.list || FALLBACK
|
||||||
if (filters.nickName) rows = rows.filter(r => r.nickName.includes(filters.nickName))
|
total.value = Number(resp?.total || 0)
|
||||||
list.value = rows
|
|
||||||
total.value = rows.length
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.msg || e?.message || '加载失败'
|
error.value = e?.msg || e?.message || '加载失败'
|
||||||
list.value = FALLBACK
|
list.value = FALLBACK
|
||||||
@ -63,6 +69,7 @@ async function load() {
|
|||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 事件 ==========
|
||||||
function onSearch() { page.value = 1; load() }
|
function onSearch() { page.value = 1; load() }
|
||||||
function onReset() { filters.nickName = ''; load() }
|
function onReset() { filters.nickName = ''; load() }
|
||||||
function onPageChange(p: number) { page.value = p; load() }
|
function onPageChange(p: number) { page.value = p; load() }
|
||||||
@ -80,24 +87,16 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="意见反馈" :sub="`用户反馈信息管理 (共 ${total} 条)`" />
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">意见反馈</h2>
|
|
||||||
<p class="rr-page-sub">用户反馈信息管理 (共 {{ total }} 条)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
<SearchBar
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
v-model:filters="filters"
|
||||||
<el-form-item label="用户昵称">
|
:fields="[
|
||||||
<el-input v-model="filters.nickName" placeholder="请输入用户昵称" clearable style="width: 240px" />
|
{ key: 'nickName', label: '用户昵称', type: 'text', placeholder: '请输入用户昵称' }
|
||||||
</el-form-item>
|
]"
|
||||||
<el-form-item>
|
@search="onSearch"
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
@reset="onReset"
|
||||||
<el-button @click="onReset">重置</el-button>
|
/>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -116,7 +115,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="total > 10"
|
:show-pagination="total > 10" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无反馈"
|
empty-text="暂无反馈"
|
||||||
borderless
|
borderless
|
||||||
@ -164,11 +163,6 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.feedback-imgs { display: flex; gap: 8px; flex-wrap: wrap; }
|
.feedback-imgs { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
.feedback-img-item { width: 80px; height: 80px; border-radius: 6px; border: 1px solid var(--border); }
|
.feedback-img-item { width: 80px; height: 80px; border-radius: 6px; border: 1px solid var(--border); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 帮助中心 - W6 P0 完整化
|
* F006 帮助中心 - W10 A1 标准化
|
||||||
|
*
|
||||||
|
* W10 A1: 套 PageHeader/SearchBar 标准化模板
|
||||||
*
|
*
|
||||||
* 沿用老 vue api/help.vue 60 行:
|
* 沿用老 vue api/help.vue 60 行:
|
||||||
* - 搜索: 标题
|
* - 搜索: 标题
|
||||||
@ -12,6 +14,8 @@ import { ref, onMounted, reactive } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Help {
|
interface Help {
|
||||||
id: string
|
id: string
|
||||||
@ -84,27 +88,18 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="帮助中心" :sub="`常见问题与帮助文档管理 (共 ${total} 条)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">帮助中心</h2>
|
|
||||||
<p class="rr-page-sub">常见问题与帮助文档管理 (共 {{ total }} 条)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" @click="onAdd">新增文档</el-button>
|
<el-button type="primary" @click="onAdd">新增文档</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
<SearchBar
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
v-model:filters="filters"
|
||||||
<el-form-item label="标题">
|
:fields="[
|
||||||
<el-input v-model="filters.title" placeholder="请输入标题" clearable style="width: 280px" />
|
{ key: 'title', label: '标题', type: 'text', placeholder: '请输入标题' }
|
||||||
</el-form-item>
|
]"
|
||||||
<el-form-item>
|
@search="onSearch"
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
@reset="onReset"
|
||||||
<el-button @click="onReset">重置</el-button>
|
/>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -120,7 +115,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="total > 10"
|
:show-pagination="total > 10" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无帮助文档"
|
empty-text="暂无帮助文档"
|
||||||
borderless
|
borderless
|
||||||
@ -158,9 +153,4 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 轮播图管理 - W6 P0 完整化
|
* F006 轮播图管理 - W10 A1 标准化
|
||||||
|
*
|
||||||
|
* W10 A1: 套 PageHeader/SearchBar 标准化模板
|
||||||
*
|
*
|
||||||
* 沿用老 vue api/lunbo.vue 88 行:
|
* 沿用老 vue api/lunbo.vue 88 行:
|
||||||
* - 搜索: 标题 / 展示位置
|
* - 搜索: 标题 / 展示位置
|
||||||
@ -12,6 +14,8 @@ import { ref, onMounted, reactive } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Lunbo {
|
interface Lunbo {
|
||||||
id: string
|
id: string
|
||||||
@ -62,12 +66,9 @@ async function load() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await new Promise(r => setTimeout(r, 200))
|
const resp: any = await httpGet('/other/lunbo/page', { page: page.value, limit: limit.value })
|
||||||
let rows = [...FALLBACK]
|
list.value = resp?.list || FALLBACK
|
||||||
if (filters.title) rows = rows.filter(r => r.title.includes(filters.title))
|
total.value = Number(resp?.total || 0)
|
||||||
if (filters.position) rows = rows.filter(r => r.position === Number(filters.position))
|
|
||||||
list.value = rows
|
|
||||||
total.value = rows.length
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.msg || e?.message || '加载失败'
|
error.value = e?.msg || e?.message || '加载失败'
|
||||||
list.value = FALLBACK
|
list.value = FALLBACK
|
||||||
@ -100,33 +101,22 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="轮播图管理" :sub="`App/小程序首页与商城轮播图配置 (共 ${total} 张)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">轮播图管理</h2>
|
|
||||||
<p class="rr-page-sub">App/小程序首页与商城轮播图配置 (共 {{ total }} 张)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" @click="onAdd">新增轮播</el-button>
|
<el-button type="primary" @click="onAdd">新增轮播</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
<SearchBar
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
v-model:filters="filters"
|
||||||
<el-form-item label="标题">
|
:fields="[
|
||||||
<el-input v-model="filters.title" placeholder="请输入标题" clearable style="width: 220px" />
|
{ key: 'title', label: '标题', type: 'text', placeholder: '请输入标题' },
|
||||||
</el-form-item>
|
{ key: 'position', label: '展示位置', type: 'select', options: [
|
||||||
<el-form-item label="展示位置">
|
{ value: 1, label: '首页' },
|
||||||
<el-select v-model="filters.position" placeholder="全部" clearable style="width: 160px">
|
{ value: 2, label: '在线商城' }
|
||||||
<el-option label="首页" :value="1" />
|
] }
|
||||||
<el-option label="在线商城" :value="2" />
|
]"
|
||||||
</el-select>
|
@search="onSearch"
|
||||||
</el-form-item>
|
@reset="onReset"
|
||||||
<el-form-item>
|
/>
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
|
||||||
<el-button @click="onReset">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -144,7 +134,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="total > 10"
|
:show-pagination="total > 10" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无轮播图"
|
empty-text="暂无轮播图"
|
||||||
borderless
|
borderless
|
||||||
@ -205,10 +195,5 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.lunbo-thumb { width: 80px; height: 60px; border-radius: 4px; border: 1px solid var(--border); }
|
.lunbo-thumb { width: 80px; height: 60px; border-radius: 4px; border: 1px solid var(--border); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const themePalette = [
|
|||||||
|
|
||||||
function refreshCaptcha() {
|
function refreshCaptcha() {
|
||||||
form.uuid = Math.random().toString(36).slice(2, 12)
|
form.uuid = Math.random().toString(36).slice(2, 12)
|
||||||
captchaUrl.value = `/auth/captcha?uuid=${form.uuid}&_t=${Date.now()}`
|
captchaUrl.value = `/api/auth/captcha?uuid=${form.uuid}&_t=${Date.now()}`
|
||||||
}
|
}
|
||||||
function onCaptchaInput(e: Event) {
|
function onCaptchaInput(e: Event) {
|
||||||
const t = e.target as HTMLInputElement
|
const t = e.target as HTMLInputElement
|
||||||
@ -65,13 +65,12 @@ async function onLogin() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
const res = await axios.post(
|
||||||
'/auth/oauth/token',
|
'/api/auth/oauth/token',
|
||||||
qs.stringify({ ...form, grant_type: 'password' }),
|
qs.stringify({ ...form, grant_type: 'password' }),
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
Authorization: 'Basic cmVucmVuaW86cmVucmVuaW8=',
|
Authorization: 'Basic cmVucmVuaW86cmVucmVuaW8='
|
||||||
client: 'front'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -542,13 +542,14 @@ const transactionMock = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// ========== user/message ==========
|
// ========== user/message ==========
|
||||||
|
// W13.4: 后端字段是 name/content/readFlag/createDate, 不是 title/receiver/status/sendTime
|
||||||
const messageCols: MockColumn[] = [
|
const messageCols: MockColumn[] = [
|
||||||
{ prop: 'id', label: 'ID', width: 80 },
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
{ prop: 'title', label: '消息标题', minWidth: 220 },
|
{ prop: 'name', label: '消息标题', minWidth: 200 },
|
||||||
{ prop: 'type', label: '类型', width: 100 },
|
{ prop: 'content', label: '内容', minWidth: 260 },
|
||||||
{ prop: 'receiver', label: '接收人', width: 140 },
|
{ prop: 'type', label: '类型', width: 80, format: (r: any) => r.type === 1 ? '系统' : r.type === 2 ? '业务' : String(r.type ?? '--') },
|
||||||
{ prop: 'status', label: '状态', width: 100, type: 'status' },
|
{ prop: 'readFlag', label: '状态', width: 80, format: (r: any) => r.readFlag === 1 ? '已读' : r.readFlag === 0 ? '未读' : String(r.readFlag ?? '--') },
|
||||||
{ prop: 'sendTime', label: '发送时间', width: 180, type: 'text' }
|
{ prop: 'createDate', label: '发送时间', width: 180, type: 'text' }
|
||||||
]
|
]
|
||||||
const messageMock = [
|
const messageMock = [
|
||||||
{ id: '1', title: '订单已发货', type: '订单通知', receiver: '深圳科技公司', status: 1, sendTime: now(0) },
|
{ id: '1', title: '订单已发货', type: '订单通知', receiver: '深圳科技公司', status: 1, sendTime: now(0) },
|
||||||
@ -757,13 +758,13 @@ export const MOCK_TABLE_CONFIGS: Record<string, MockConfig> = {
|
|||||||
'user-enterprise-score': { endpoint: '/user/enterprise/enterprise-score/page', title: '评分', columns: enterpriseScoreCols, mockData: enterpriseScoreMock, queryKey: 'enterpriseName', queryLabel: '企业名称' },
|
'user-enterprise-score': { endpoint: '/user/enterprise/enterprise-score/page', title: '评分', columns: enterpriseScoreCols, mockData: enterpriseScoreMock, queryKey: 'enterpriseName', queryLabel: '企业名称' },
|
||||||
|
|
||||||
// user/finance (4)
|
// user/finance (4)
|
||||||
'user-finance-recharge': { endpoint: '/user/finance/recharge/page', title: '充值管理', columns: rechargeCols, mockData: rechargeMock, queryKey: 'orderNo', queryLabel: '订单号' },
|
'user-finance-recharge': { endpoint: '/user/accounttransaction/page', title: '充值管理', columns: rechargeCols, mockData: rechargeMock, queryKey: 'orderNo', queryLabel: '订单号' },
|
||||||
'user-finance-recharge-tx': { endpoint: '/user/finance/recharge-tx/page', title: '提现管理', columns: rechargeTxCols, mockData: rechargeTxMock, queryKey: 'orderNo', queryLabel: '订单号' },
|
'user-finance-recharge-tx': { endpoint: '/user/accounttransaction/page', title: '提现管理', columns: rechargeTxCols, mockData: rechargeTxMock, queryKey: 'orderNo', queryLabel: '订单号' },
|
||||||
'user-finance-revenue': { endpoint: '/user/finance/revenue-sharing/page', title: '分账记录管理', columns: revenueCols, mockData: revenueMock },
|
'user-finance-revenue': { endpoint: '/user/accounttransaction/page', title: '分账记录管理', columns: revenueCols, mockData: revenueMock },
|
||||||
'user-finance-transaction': { endpoint: '/user/finance/transaction-record/page', title: '交易流水', columns: transactionCols, mockData: transactionMock, queryKey: 'orderNo', queryLabel: '关联单号' },
|
'user-finance-transaction': { endpoint: '/user/accounttransaction/page', title: '交易流水', columns: transactionCols, mockData: transactionMock, queryKey: 'orderNo', queryLabel: '关联单号' },
|
||||||
|
|
||||||
// user/message (2)
|
// user/message (2)
|
||||||
'user-message': { endpoint: '/user/message/message/page', title: '系统消息', columns: messageCols, mockData: messageMock, queryKey: 'title', queryLabel: '消息标题' },
|
'user-message': { endpoint: '/user/message/page', title: '系统消息', columns: messageCols, mockData: messageMock, queryKey: 'title', queryLabel: '消息标题' },
|
||||||
'user-message-handel': { endpoint: '/user/message/messageHandel/page', title: '待处理消息', columns: messageHandelCols, mockData: messageHandelMock, queryKey: 'title', queryLabel: '事项' },
|
'user-message-handel': { endpoint: '/user/message/messageHandel/page', title: '待处理消息', columns: messageHandelCols, mockData: messageHandelMock, queryKey: 'title', queryLabel: '事项' },
|
||||||
|
|
||||||
// sys/log (3)
|
// sys/log (3)
|
||||||
@ -779,7 +780,7 @@ export const MOCK_TABLE_CONFIGS: Record<string, MockConfig> = {
|
|||||||
'sys-userManagement': { endpoint: '/sys/userManagement/page', title: '全国运营中心', columns: operationCols, mockData: userManagementMock, queryKey: 'name', queryLabel: '运营中心' },
|
'sys-userManagement': { endpoint: '/sys/userManagement/page', title: '全国运营中心', columns: operationCols, mockData: userManagementMock, queryKey: 'name', queryLabel: '运营中心' },
|
||||||
|
|
||||||
// sys (4)
|
// sys (4)
|
||||||
'sys-dict': { endpoint: '/sys/dict-type/page', title: '字典类型', columns: dictTypeCols, mockData: dictTypeMock, queryKey: 'dictType', queryLabel: '字典类型' },
|
'sys-dict': { endpoint: '/sys/dict/type/page', title: '字典类型', columns: dictTypeCols, mockData: dictTypeMock, queryKey: 'dictType', queryLabel: '字典类型' },
|
||||||
'sys-notice': { endpoint: '/sys/notice/page', title: '通知公告', columns: noticeCols, mockData: noticeMock, queryKey: 'title', queryLabel: '标题' },
|
'sys-notice': { endpoint: '/sys/notice/page', title: '通知公告', columns: noticeCols, mockData: noticeMock, queryKey: 'title', queryLabel: '标题' },
|
||||||
'sys-params': { endpoint: '/sys/params/page', title: '参数配置', columns: paramsCols, mockData: paramsMock, queryKey: 'paramKey', queryLabel: '参数键' },
|
'sys-params': { endpoint: '/sys/params/page', title: '参数配置', columns: paramsCols, mockData: paramsMock, queryKey: 'paramKey', queryLabel: '参数键' },
|
||||||
'sys-notice-user': { endpoint: '/sys/notice-user/page', title: '用户通知', columns: noticeUserCols, mockData: noticeUserMock, queryKey: 'userName', queryLabel: '接收人' }
|
'sys-notice-user': { endpoint: '/sys/notice-user/page', title: '用户通知', columns: noticeUserCols, mockData: noticeUserMock, queryKey: 'userName', queryLabel: '接收人' }
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { ref, onMounted, watch, computed } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
interface Inquiry {
|
interface Inquiry {
|
||||||
id: string
|
id: string
|
||||||
@ -122,9 +123,9 @@ watch(inquiryId, load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="询价详情" :sub="`询价单号: ${detail?.id || ''} · 发布时间: ${detail ? fmtDate(detail.createDate) : ''}`">
|
||||||
<el-button text @click="onBack">← 返回询价列表</el-button>
|
<el-button text @click="onBack">← 返回询价列表</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
|
|
||||||
<div v-if="loading" class="rr-loading"><el-skeleton :rows="6" animated /></div>
|
<div v-if="loading" class="rr-loading"><el-skeleton :rows="6" animated /></div>
|
||||||
<div v-else-if="!detail" class="rr-empty"><el-empty description="询价不存在" /></div>
|
<div v-else-if="!detail" class="rr-empty"><el-empty description="询价不存在" /></div>
|
||||||
@ -212,7 +213,6 @@ watch(inquiryId, load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
||||||
.rr-card-header { padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
.rr-card-header { padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
||||||
.rr-card-title { font-size: 15px; font-weight: 600; color: var(--ink); margin: 0; }
|
.rr-card-title { font-size: 15px; font-weight: 600; color: var(--ink); margin: 0; }
|
||||||
|
|||||||
@ -7,10 +7,13 @@
|
|||||||
*
|
*
|
||||||
* 设计规范:§16 列表页完整模板 + §17.7 紧凑弹窗 + §17.8 主题色弹窗 + §17.9 列优化
|
* 设计规范:§16 列表页完整模板 + §17.7 紧凑弹窗 + §17.8 主题色弹窗 + §17.9 列优化
|
||||||
*/
|
*/
|
||||||
import { onMounted, computed, ref } from 'vue'
|
import { onMounted, computed, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||||||
import { useInquiryStore, STATUS_OPTIONS } from '@/stores/inquiry'
|
import { useInquiryStore, STATUS_OPTIONS } from '@/stores/inquiry'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
|
||||||
@ -27,7 +30,10 @@ interface InquiryRow {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useInquiryStore()
|
const store = useInquiryStore()
|
||||||
|
|
||||||
const STATUS_TABS = STATUS_OPTIONS.map(o => ({ value: o.value, label: o.label }))
|
const STATUS_TABS = STATUS_OPTIONS.map(o => ({ value: o.value, label: o.label, key: o.value, count: 0 }))
|
||||||
|
|
||||||
|
// 各 status 计数 (在当前结果中累加)
|
||||||
|
const stats = reactive<Record<string, number>>({ pending: 0, quoting: 0, done: 0, cancelled: 0, all: 0 })
|
||||||
|
|
||||||
const STATUS_LABEL: Record<string, string> = {
|
const STATUS_LABEL: Record<string, string> = {
|
||||||
pending: '待报价',
|
pending: '待报价',
|
||||||
@ -84,6 +90,9 @@ async function load() {
|
|||||||
raw: it
|
raw: it
|
||||||
}))
|
}))
|
||||||
total.value = arr.length + (page.value - 1) * limit.value
|
total.value = arr.length + (page.value - 1) * limit.value
|
||||||
|
// 各状态计数 (基于当前页结果)
|
||||||
|
stats.all = list.value.length
|
||||||
|
list.value.forEach(r => { stats[r.status] = (stats[r.status] || 0) + 1 })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.msg || e?.message || '后端接口异常(详见 console)'
|
error.value = e?.msg || e?.message || '后端接口异常(详见 console)'
|
||||||
list.value = []
|
list.value = []
|
||||||
@ -94,6 +103,12 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSearch() { page.value = 1; load() }
|
function onSearch() { page.value = 1; load() }
|
||||||
|
|
||||||
|
const tabItems = computed(() => STATUS_TABS.map(t => ({
|
||||||
|
key: t.value,
|
||||||
|
label: t.label,
|
||||||
|
count: t.value === '' ? stats.all : (stats[t.value] || 0)
|
||||||
|
})))
|
||||||
function onReset() { filters.value.status = ''; filters.value.customer = ''; filters.value.searchKey = ''; page.value = 1; load() }
|
function onReset() { filters.value.status = ''; filters.value.customer = ''; filters.value.searchKey = ''; page.value = 1; load() }
|
||||||
function onPageChange(p: number) { page.value = p; load() }
|
function 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() }
|
||||||
@ -120,39 +135,18 @@ onMounted(() => { load() })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-inquiry-list">
|
<div class="rr-inquiry-list">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="询价管理" :sub="`共 ${total} 条询价`">
|
||||||
<div class="rr-page-titles">
|
<el-button @click="onReset">重置</el-button>
|
||||||
<h2 class="rr-page-title">询价管理</h2>
|
</PageHeader>
|
||||||
<p class="rr-page-sub">共 {{ total }} 条询价</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-tabs-row">
|
<ListTabs v-model="filters.status" :items="tabItems" @change="onStatusTab" />
|
||||||
<div class="rr-tabs">
|
|
||||||
<div
|
<SearchBar
|
||||||
v-for="t in STATUS_TABS"
|
v-model:filters="filters"
|
||||||
:key="t.value"
|
:fields='[{ key: "searchKey", label: "", type: "text", placeholder: "搜索单号/客户/商品", clearable: true }]'
|
||||||
class="rr-tab"
|
:show-reset="false"
|
||||||
:class="{ active: filters.status === t.value }"
|
@search="onSearch"
|
||||||
@click="onStatusTab(t.value)"
|
|
||||||
>{{ t.label }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="rr-filters">
|
|
||||||
<el-input
|
|
||||||
v-model="filters.searchKey"
|
|
||||||
placeholder="搜索单号/客户/商品"
|
|
||||||
clearable
|
|
||||||
class="rr-search-input"
|
|
||||||
size="small"
|
|
||||||
:prefix-icon="'Search'"
|
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
/>
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -162,7 +156,7 @@ onMounted(() => { load() })
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无询价"
|
empty-text="暂无询价"
|
||||||
borderless
|
borderless
|
||||||
@ -190,91 +184,6 @@ onMounted(() => { load() })
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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-title {
|
|
||||||
margin: 0;
|
|
||||||
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-tabs-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-tab {
|
|
||||||
position: relative;
|
|
||||||
padding: 6px 0;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
transition: color 0.15s ease;
|
|
||||||
}
|
|
||||||
.rr-tab:hover { color: var(--el-text-color-primary); }
|
|
||||||
.rr-tab.active {
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.rr-tab.active::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: var(--el-color-primary);
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-search-input { width: 240px; }
|
|
||||||
|
|
||||||
.rr-list-card {
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-time {
|
.rr-time {
|
||||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -292,8 +201,8 @@ onMounted(() => { load() })
|
|||||||
height: 21.25px;
|
height: 21.25px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.rr-badge-pending { background: #FEF3C7; color: #D97706; }
|
.rr-badge-pending { background: var(--amber-soft); color: var(--amber); }
|
||||||
.rr-badge-quoting { background: #DBEAFE; color: #2563EB; }
|
.rr-badge-quoting { background: var(--brand-soft); color: var(--brand); }
|
||||||
.rr-badge-done { background: #D1FAE5; color: #059669; }
|
.rr-badge-done { background: var(--green-soft); color: var(--green); }
|
||||||
.rr-badge-cancelled { background: #FEE2E2; color: #DC2626; }
|
.rr-badge-cancelled { background: var(--red-soft); color: var(--red); }
|
||||||
</style>
|
</style>
|
||||||
49
src/modules/message/maillog/MailLogListView.vue
Normal file
49
src/modules/message/maillog/MailLogListView.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 邮件日志 - P0 真 view
|
||||||
|
* 走 /sys/maillog/page (2026-08-07 探测: 200, total=0, 接口正常暂无数据)
|
||||||
|
* 字段(推测): id/mailCode/platform/toEmail/subject/content/status/creator/createDate
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface MailLog { id: string; mailCode: string; platform: number; toEmail: string; subject: string; content: string; status: number; creator: number; createDate: string }
|
||||||
|
const STATUS_MAP: Record<number, { label: string; type: string }> = { 0: { label: '失败', type: 'danger' }, 1: { label: '成功', type: 'success' }, 2: { label: '等待', type: 'warning' } }
|
||||||
|
const list = ref<MailLog[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
|
{ prop: 'mailCode', label: '邮件编码', width: 130 },
|
||||||
|
{ prop: 'toEmail', label: '收件邮箱', minWidth: 180 },
|
||||||
|
{ prop: 'subject', label: '主题', minWidth: 200 },
|
||||||
|
{ prop: 'status', label: '状态', width: 90, slot: 'status' },
|
||||||
|
{ prop: 'creator', label: '操作人', width: 90 },
|
||||||
|
{ prop: 'createDate', label: '发送时间', width: 180 }
|
||||||
|
]
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try { const r: any = await httpGet('/sys/maillog/page', { page: query.page, limit: query.limit }); list.value = r?.list || []; total.value = Number(r?.total || 0) }
|
||||||
|
catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="邮件日志" :sub="`共 ${total} 条`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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" @limit-change="onLimitChange">
|
||||||
|
<template #status="{ row }"><el-tag :type="(STATUS_MAP[row.status]?.type as any) || 'info'">{{ STATUS_MAP[row.status]?.label || row.status }}</el-tag></template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
44
src/modules/message/mailtemplate/MailTemplateListView.vue
Normal file
44
src/modules/message/mailtemplate/MailTemplateListView.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 邮件模板 - P0 真 view
|
||||||
|
* 走 /sys/mailtemplate/page (2026-08-07 探测: 200, total=1)
|
||||||
|
* 字段: id/name/subject/content/createDate
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface MailTpl { id: string; name: string; subject: string; content: string; createDate: string }
|
||||||
|
const list = ref<MailTpl[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
|
{ prop: 'name', label: '模板名称', minWidth: 200 },
|
||||||
|
{ prop: 'subject', label: '邮件主题', minWidth: 200 },
|
||||||
|
{ prop: 'content', label: '内容', minWidth: 200, formatter: (r: MailTpl) => r.content ? r.content.slice(0, 50) + '...' : '--' },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 180 }
|
||||||
|
]
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try { const r: any = await httpGet('/sys/mailtemplate/page', { page: query.page, limit: query.limit }); list.value = r?.list || []; total.value = Number(r?.total || 0) }
|
||||||
|
catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="邮件模板" :sub="`共 ${total} 条`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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" @limit-change="onLimitChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
48
src/modules/message/sms/SmsListView.vue
Normal file
48
src/modules/message/sms/SmsListView.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 短信配置 - P0 真 view
|
||||||
|
* 走 /sys/sms/page (2026-08-07 探测: 200, total=2)
|
||||||
|
* 字段: id/smsCode/platform/remark/config/createDate
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface Sms { id: string; smsCode: string; platform: number; remark: string; config: string; createDate: string }
|
||||||
|
const PLATFORM_MAP: Record<number, { label: string; type: string }> = { 0: { label: '阿里云', type: 'success' }, 1: { label: '腾讯云', type: 'success' }, 2: { label: '七牛云', type: 'success' }, 4: { label: '又拍云', type: 'success' } }
|
||||||
|
const list = ref<Sms[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
|
{ prop: 'smsCode', label: '短信编码', minWidth: 150 },
|
||||||
|
{ prop: 'platform', label: '平台', width: 120, slot: 'platform' },
|
||||||
|
{ prop: 'remark', label: '备注', minWidth: 200, formatter: (r: Sms) => r.remark || '--' },
|
||||||
|
{ prop: 'config', label: '配置', minWidth: 200, formatter: (r: Sms) => r.config ? r.config.slice(0, 50) + '...' : '--' },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 180 }
|
||||||
|
]
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try { const r: any = await httpGet('/sys/sms/page', { page: query.page, limit: query.limit }); list.value = r?.list || []; total.value = Number(r?.total || 0) }
|
||||||
|
catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="短信配置" :sub="`共 ${total} 条`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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" @limit-change="onLimitChange">
|
||||||
|
<template #platform="{ row }"><el-tag :type="(PLATFORM_MAP[row.platform]?.type as any) || 'info'">{{ PLATFORM_MAP[row.platform]?.label || row.platform }}</el-tag></template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
49
src/modules/message/smslog/SmsLogListView.vue
Normal file
49
src/modules/message/smslog/SmsLogListView.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 短信日志 - P0 真 view
|
||||||
|
* 走 /sys/smslog/page (2026-08-07 探测: 200, total=1275)
|
||||||
|
* 字段: id/smsCode/platform/mobile/params1-4/status/creator/createDate
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface SmsLog { id: string; smsCode: string; platform: number; mobile: string; params1: string; params2: string; params3: string; params4: string; status: number; creator: number; createDate: string }
|
||||||
|
const STATUS_MAP: Record<number, { label: string; type: string }> = { 0: { label: '失败', type: 'danger' }, 1: { label: '成功', type: 'success' }, 2: { label: '等待', type: 'warning' } }
|
||||||
|
const list = ref<SmsLog[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
|
{ prop: 'smsCode', label: '短信编码', width: 130 },
|
||||||
|
{ prop: 'mobile', label: '手机号', width: 140 },
|
||||||
|
{ prop: 'params1', label: '参数1', minWidth: 120, formatter: (r: SmsLog) => r.params1 || '--' },
|
||||||
|
{ prop: 'status', label: '状态', width: 90, slot: 'status' },
|
||||||
|
{ prop: 'creator', label: '操作人', width: 90 },
|
||||||
|
{ prop: 'createDate', label: '发送时间', width: 180 }
|
||||||
|
]
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try { const r: any = await httpGet('/sys/smslog/page', { page: query.page, limit: query.limit }); list.value = r?.list || []; total.value = Number(r?.total || 0) }
|
||||||
|
catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="短信日志" :sub="`共 ${total} 条`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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" @limit-change="onLimitChange">
|
||||||
|
<template #status="{ row }"><el-tag :type="(STATUS_MAP[row.status]?.type as any) || 'info'">{{ STATUS_MAP[row.status]?.label || row.status }}</el-tag></template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
@ -16,7 +16,7 @@
|
|||||||
*
|
*
|
||||||
* 后端调 /sys/openapi/app/apply
|
* 后端调 /sys/openapi/app/apply
|
||||||
*/
|
*/
|
||||||
import { ref, reactive, computed, watch } from 'vue'
|
import { ref, reactive, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { httpGet, httpPost } from '@/utils/request'
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
|
||||||
@ -32,9 +32,8 @@ interface ApplyResult {
|
|||||||
customerName: string
|
customerName: string
|
||||||
customerCode: string
|
customerCode: string
|
||||||
appId: string
|
appId: string
|
||||||
connectCode: string
|
|
||||||
appSecret: string
|
appSecret: string
|
||||||
expireAt: string
|
mcpEndpoint: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: boolean }>()
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
@ -71,10 +70,14 @@ const form = reactive({
|
|||||||
const customers = ref<Customer[]>([])
|
const customers = ref<Customer[]>([])
|
||||||
const loadingCustomers = ref(false)
|
const loadingCustomers = ref(false)
|
||||||
|
|
||||||
async function loadCustomers() {
|
// F002-W18 修: 后端已支持 name 模糊搜, 前端把 searchKey 传给后端, 不再本地 filter
|
||||||
|
async function loadCustomers(searchKey?: string) {
|
||||||
loadingCustomers.value = true
|
loadingCustomers.value = true
|
||||||
try {
|
try {
|
||||||
const data = await httpGet<Customer[]>('/sys/openapi/customer/list')
|
const data = await httpGet<Customer[]>('/sys/openapi/customer/list', {
|
||||||
|
name: searchKey || undefined,
|
||||||
|
limit: 1000
|
||||||
|
})
|
||||||
customers.value = Array.isArray(data) ? data : []
|
customers.value = Array.isArray(data) ? data : []
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(e?.msg || e?.message || '加载客户列表失败')
|
ElMessage.error(e?.msg || e?.message || '加载客户列表失败')
|
||||||
@ -86,13 +89,13 @@ async function loadCustomers() {
|
|||||||
|
|
||||||
watch(visible, v => { if (v) loadCustomers() })
|
watch(visible, v => { if (v) loadCustomers() })
|
||||||
|
|
||||||
const filteredCustomers = computed(() => {
|
// F002-W18 修: searchKey 变化 debounce 300ms 调后端
|
||||||
if (!form.searchKey) return customers.value
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const k = form.searchKey.toLowerCase()
|
watch(() => form.searchKey, (val) => {
|
||||||
return customers.value.filter(c =>
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
(c.customerName || '').toLowerCase().includes(k) ||
|
searchTimer = setTimeout(() => {
|
||||||
(c.customerCode || '').toLowerCase().includes(k)
|
loadCustomers(val || undefined)
|
||||||
)
|
}, 300)
|
||||||
})
|
})
|
||||||
|
|
||||||
function onSelectionChange(rows: Customer[]) {
|
function onSelectionChange(rows: Customer[]) {
|
||||||
@ -129,7 +132,6 @@ async function submit() {
|
|||||||
const res = await httpPost<ApplyResult[]>('/sys/openapi/app/apply', payload)
|
const res = await httpPost<ApplyResult[]>('/sys/openapi/app/apply', payload)
|
||||||
result.value = Array.isArray(res) ? res : []
|
result.value = Array.isArray(res) ? res : []
|
||||||
ElMessage.success(`已为 ${result.value.length} 个客户生成 APIKey`)
|
ElMessage.success(`已为 ${result.value.length} 个客户生成 APIKey`)
|
||||||
emit('refresh')
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(e?.msg || e?.message || '申请失败')
|
ElMessage.error(e?.msg || e?.message || '申请失败')
|
||||||
} finally {
|
} finally {
|
||||||
@ -137,14 +139,38 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClose() { updateVisible(false) }
|
function onClose() { updateVisible(false); if (result.value) emit("refresh") }
|
||||||
|
|
||||||
async function copySecret(text: string) {
|
async function copySecret(text: string) {
|
||||||
|
// 优先用现代 Clipboard API (HTTPS / localhost 才允许)
|
||||||
|
// frp 走 http://direv.cloud:8080 是非安全上下文, clipboard API 不可用, 需降级
|
||||||
try {
|
try {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
ElMessage.success('已复制')
|
ElMessage.success('已复制')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw new Error('no clipboard api')
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('复制失败,请手动选择文本')
|
// 降级方案: document.execCommand('copy') - HTTP 下也能用
|
||||||
|
try {
|
||||||
|
const ta = document.createElement('textarea')
|
||||||
|
ta.value = text
|
||||||
|
ta.style.position = 'fixed'
|
||||||
|
ta.style.opacity = '0'
|
||||||
|
ta.style.pointerEvents = 'none'
|
||||||
|
document.body.appendChild(ta)
|
||||||
|
ta.select()
|
||||||
|
const ok = document.execCommand('copy')
|
||||||
|
document.body.removeChild(ta)
|
||||||
|
if (ok) {
|
||||||
|
ElMessage.success('已复制')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('浏览器阻止自动复制,请手动双击输入框选中后 Cmd/Ctrl+C')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('浏览器阻止自动复制,请手动双击输入框选中后 Cmd/Ctrl+C')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,20 +250,21 @@ function notifyContact() {
|
|||||||
/>
|
/>
|
||||||
<el-table
|
<el-table
|
||||||
ref="customerTableRef"
|
ref="customerTableRef"
|
||||||
:data="filteredCustomers"
|
:data="customers"
|
||||||
border
|
border
|
||||||
size="small"
|
size="small"
|
||||||
max-height="320"
|
max-height="320"
|
||||||
:loading="loadingCustomers"
|
:loading="loadingCustomers"
|
||||||
:row-style="{ height: '36px' }"
|
:row-style="{ height: '36px' }"
|
||||||
:cell-style="{ padding: '4px 8px', fontSize: '12.5px' }"
|
:cell-style="{ padding: '4px 8px', fontSize: '12.5px' }"
|
||||||
:header-cell-style="{ padding: '6px 8px', fontSize: '12px', height: '32px', background: '#F8FAFC' }"
|
:header-cell-style="{ padding: '6px 8px', fontSize: '12px', height: '32px', background: 'var(--surface-2)' }"
|
||||||
|
class="rr-apply-table"
|
||||||
@selection-change="onSelectionChange"
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" header-align="center" align="center" width="40" />
|
<el-table-column type="selection" header-align="center" align="center" width="48" />
|
||||||
<el-table-column prop="customerName" label="客户名" min-width="180" header-align="center" align="center" show-overflow-tooltip />
|
<el-table-column prop="customerName" label="公司名称" min-width="200" header-align="left" align="left" show-overflow-tooltip />
|
||||||
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
|
<el-table-column prop="contactName" label="联系人" width="100" header-align="center" align="center" />
|
||||||
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
|
<el-table-column prop="contactPhone" label="联系电话" width="140" header-align="center" align="center" />
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -320,33 +347,34 @@ function notifyContact() {
|
|||||||
<el-descriptions-item label="备注">{{ form.remark || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="备注">{{ form.remark || '—' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
<el-alert type="info" :closable="false" style="margin-top: 16px;">
|
<el-alert type="info" :closable="false" style="margin-top: 16px;">
|
||||||
后端将自动创建公司账号 + 生成 connect_code(2h 有效)
|
后端将自动创建公司账号 + 生成 APIKey
|
||||||
</el-alert>
|
</el-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 3: 生成结果 -->
|
<!-- Step 3: 生成结果 -->
|
||||||
<div v-if="step === 3 && result">
|
<div v-if="step === 3 && result">
|
||||||
<el-alert type="warning" :closable="false" style="margin-bottom: 16px;">
|
<el-alert type="warning" :closable="false" style="margin-bottom: 16px;">
|
||||||
请立即复制 connectCode 和 appSecret!关弹窗后无法再次查看
|
请立即复制 MCP地址、appId 和 appSecret!关弹窗后 appSecret 无法再次查看
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<p>已为 {{ result.length }} 个客户生成 APIKey:</p>
|
<p>已为 {{ result.length }} 个客户生成 APIKey:</p>
|
||||||
<el-table :data="result" border>
|
<el-table :data="result" border class="rr-apply-table rr-result-table" size="small">
|
||||||
<el-table-column prop="customerName" label="客户名" width="160" />
|
<el-table-column prop="customerName" label="公司名称" width="120" show-overflow-tooltip />
|
||||||
<el-table-column prop="appId" label="AppID" width="280">
|
<el-table-column prop="appId" label="AppID" min-width="180" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<code>{{ row.appId }}</code>
|
<code class="rr-cell-code">{{ row.appId }}</code>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="connectCode" label="ConnectCode">
|
<el-table-column prop="mcpEndpoint" label="MCP地址" min-width="220" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<code class="rr-code-accent">{{ row.connectCode }}</code>
|
<code class="rr-cell-code">{{ row.mcpEndpoint || '—' }}</code>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="expireAt" label="过期时间" width="180" />
|
<el-table-column label="AppSecret" min-width="200">
|
||||||
<el-table-column label="AppSecret" min-width="320">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-input :model-value="row.appSecret" readonly type="password" size="small" show-password style="width: 220px; margin-right: 8px;" />
|
<div class="rr-secret-cell">
|
||||||
|
<el-input :model-value="row.appSecret" readonly type="password" size="small" show-password />
|
||||||
<el-button size="small" type="primary" @click="copySecret(row.appSecret)">复制</el-button>
|
<el-button size="small" type="primary" @click="copySecret(row.appSecret)">复制</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -478,4 +506,43 @@ function notifyContact() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rr-code-accent { color: var(--red); font-family: ui-monospace, monospace; }
|
.rr-code-accent { color: var(--red); font-family: ui-monospace, monospace; }
|
||||||
|
|
||||||
|
/* 弹窗内表格圆角 + 边框颜色统一 (修复圆角缺失) */
|
||||||
|
:deep(.rr-apply-dialog .rr-apply-table) {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border) !important;
|
||||||
|
}
|
||||||
|
:deep(.rr-apply-dialog .rr-apply-table .el-table__inner-wrapper) {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
/* selection 列 checkbox 居中 */
|
||||||
|
:deep(.rr-apply-dialog .rr-apply-table .el-table-column--selection .cell) {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
:deep(.rr-apply-dialog .rr-apply-table th.el-table-column--selection .cell) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step3 结果表格: AppSecret 单元格紧凑布局 */
|
||||||
|
.rr-secret-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.rr-secret-cell .el-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
/* code 文本省略, 不撑列宽 */
|
||||||
|
.rr-cell-code {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import { onMounted, ref, reactive, computed } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
interface AuditLog {
|
interface AuditLog {
|
||||||
@ -293,16 +295,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-openapi-audit">
|
<div class="rr-openapi-audit">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="审计日志" :sub="`共 ${total} 条调用记录`">
|
||||||
<div class="rr-page-titles">
|
<el-button @click="onReset">重置</el-button>
|
||||||
<h2 class="rr-page-title">审计日志</h2>
|
</PageHeader>
|
||||||
<p class="rr-page-sub">共 {{ total }} 条调用记录</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-tabs-row">
|
<div class="rr-tabs-row">
|
||||||
<div class="rr-tabs">
|
<div class="rr-tabs">
|
||||||
@ -314,59 +309,23 @@ onMounted(() => {
|
|||||||
@click="onStatusTab(t.value)"
|
@click="onStatusTab(t.value)"
|
||||||
>{{ t.label }}</div>
|
>{{ t.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rr-filters">
|
|
||||||
<el-input
|
|
||||||
v-model="filters.searchKey"
|
|
||||||
placeholder="搜索 工具 / 请求ID / 摘要"
|
|
||||||
clearable
|
|
||||||
class="rr-search-input"
|
|
||||||
size="small"
|
|
||||||
:prefix-icon="'Search'"
|
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
|
||||||
<el-select
|
|
||||||
v-model="filters.customerCode"
|
|
||||||
placeholder="客户"
|
|
||||||
class="rr-customer-select"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="opt in customerOptions"
|
|
||||||
:key="opt.value"
|
|
||||||
:label="opt.label"
|
|
||||||
:value="opt.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-select
|
|
||||||
v-model="filters.appId"
|
|
||||||
placeholder="App"
|
|
||||||
class="rr-app-select"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="opt in appOptions"
|
|
||||||
:key="opt.value"
|
|
||||||
:label="opt.label"
|
|
||||||
:value="opt.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-select
|
|
||||||
v-model="filters.callType"
|
|
||||||
placeholder="类型"
|
|
||||||
class="rr-type-select"
|
|
||||||
clearable
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<el-option value="MCP" label="MCP" />
|
|
||||||
<el-option value="REST" label="REST" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SearchBar
|
||||||
|
v-model:filters="filters"
|
||||||
|
:fields='[
|
||||||
|
{ key: "searchKey", label: "", type: "text", placeholder: "搜索 工具 / 请求ID / 摘要", clearable: true },
|
||||||
|
{ key: "customerCode", label: "客户", type: "select", placeholder: "客户", clearable: true,
|
||||||
|
options: customerOptions.map(o => ({ value: o.value, label: o.label })) },
|
||||||
|
{ key: "appId", label: "App", type: "select", placeholder: "App", clearable: true,
|
||||||
|
options: appOptions.map(o => ({ value: o.value, label: o.label })) },
|
||||||
|
{ key: "callType", label: "类型", type: "select", placeholder: "类型", clearable: true,
|
||||||
|
options: [{ value: "MCP", label: "MCP" }, { value: "REST", label: "REST" }] }
|
||||||
|
]'
|
||||||
|
:show-reset="false"
|
||||||
|
@search="onSearch"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
:columns="[
|
:columns="[
|
||||||
@ -386,7 +345,7 @@ onMounted(() => {
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="loadError"
|
:error="loadError"
|
||||||
empty-text="暂无审计记录"
|
empty-text="暂无审计记录"
|
||||||
borderless
|
borderless
|
||||||
@ -533,36 +492,15 @@ onMounted(() => {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rr-page-header {
|
/* PageHeader/SearchBar 接管 */
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-titles {
|
/* PageHeader/SearchBar 接管 */
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-title {
|
/* PageHeader/SearchBar 接管 */
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-sub {
|
/* PageHeader/SearchBar 接管 */
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-actions {
|
/* PageHeader/SearchBar 接管 */
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-tabs-row {
|
.rr-tabs-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -604,16 +542,12 @@ onMounted(() => {
|
|||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rr-filters {
|
/* PageHeader/SearchBar 接管 */
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-search-input { width: 240px; }
|
/* PageHeader/SearchBar 接管 */
|
||||||
.rr-customer-select { width: 180px; }
|
/* PageHeader/SearchBar 接管 */
|
||||||
.rr-app-select { width: 180px; }
|
/* PageHeader/SearchBar 接管 */
|
||||||
.rr-type-select { width: 90px; }
|
/* PageHeader/SearchBar 接管 */
|
||||||
|
|
||||||
.rr-list-card {
|
.rr-list-card {
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
|
|||||||
@ -21,7 +21,10 @@ import { onMounted, ref, reactive } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { httpGet, httpPost } from '@/utils/request'
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface CompanyAccount {
|
interface CompanyAccount {
|
||||||
id: string
|
id: string
|
||||||
@ -239,34 +242,20 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-openapi-company">
|
<div class="rr-openapi-company">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="公司账号管理" :sub="`共 ${total} 个公司账号`">
|
||||||
<div class="rr-page-titles">
|
<el-button @click="onReset">重置</el-button>
|
||||||
<h2 class="rr-page-title">公司账号管理</h2>
|
</PageHeader>
|
||||||
<p class="rr-page-sub">共 {{ total }} 个公司账号</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-select
|
v-model:filters="filters"
|
||||||
v-model="filters.customerCode"
|
:fields='[{
|
||||||
placeholder="选择客户筛选"
|
key: "customerCode", label: "客户筛选", type: "select",
|
||||||
class="rr-customer-select"
|
placeholder: "选择客户筛选", clearable: true,
|
||||||
clearable
|
options: customerOptions.map(o => ({ value: o.value, label: o.label }))
|
||||||
filterable
|
}]'
|
||||||
size="small"
|
@search="onSearch"
|
||||||
>
|
@reset="load"
|
||||||
<el-option
|
|
||||||
v-for="opt in customerOptions"
|
|
||||||
:key="opt.value"
|
|
||||||
:label="opt.label"
|
|
||||||
:value="opt.value"
|
|
||||||
/>
|
/>
|
||||||
</el-select>
|
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -278,14 +267,14 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
{ prop: 'activitySummary', label: '动态摘要', width: 280 },
|
{ prop: 'activitySummary', label: '动态摘要', width: 280 },
|
||||||
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
||||||
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
||||||
{ label: '操作', width: 280, slot: 'action' }
|
{ label: '操作', width: 140, slot: 'action' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list" :borderless="true"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="loadError"
|
:error="loadError"
|
||||||
empty-text="暂无公司账号数据"
|
empty-text="暂无公司账号数据"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@ -311,24 +300,14 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<el-button
|
<TableActions
|
||||||
text
|
:row="row"
|
||||||
type="primary"
|
:actions="[
|
||||||
size="small"
|
{ label: '查看动态', type: 'primary', onClick: activityHandle },
|
||||||
@click="activityHandle(row)"
|
{ label: '接管', type: 'primary', onClick: takeoverHandle },
|
||||||
>查看动态</el-button>
|
{ label: '解绑', type: 'danger', onClick: unbindHandle }
|
||||||
<el-button
|
]"
|
||||||
text
|
/>
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="takeoverHandle(row)"
|
|
||||||
>接管</el-button>
|
|
||||||
<el-button
|
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="unbindHandle(row)"
|
|
||||||
>解绑</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
@ -421,50 +400,19 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rr-page-header {
|
/* PageHeader 接管 */
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-titles {
|
/* PageHeader 接管 */
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-title {
|
/* PageHeader 接管 */
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-sub {
|
/* PageHeader 接管 */
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-page-actions {
|
/* PageHeader 接管 */
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-filters-row {
|
/* SearchBar 接管 */
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-customer-select {
|
/* SearchBar 接管 */
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rr-list-card {
|
.rr-list-card {
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
|
|||||||
@ -6,10 +6,9 @@
|
|||||||
* - 操作列(根据状态显示):
|
* - 操作列(根据状态显示):
|
||||||
* * 详情(任意状态,弹窗)
|
* * 详情(任意状态,弹窗)
|
||||||
* * 重置密钥(status=0 或 1)
|
* * 重置密钥(status=0 或 1)
|
||||||
* * 重发二维码(status=0 或 1, W4.4 后端补 resend-qr)
|
|
||||||
* * 撤销(status=0 或 1)
|
* * 撤销(status=0 或 1)
|
||||||
* * 删除(status=2)
|
* * 删除(status=2)
|
||||||
* - 5 个弹窗:详情/撤销确认/删除确认/重置密钥/重发连接码
|
* - 4 个弹窗:详情/撤销确认/删除确认/重置密钥
|
||||||
* - 申请 APIKey 走 ApplyApiKeyDialog 2 步向导
|
* - 申请 APIKey 走 ApplyApiKeyDialog 2 步向导
|
||||||
*
|
*
|
||||||
* 后端端点:
|
* 后端端点:
|
||||||
@ -23,7 +22,10 @@
|
|||||||
import { onMounted, ref, reactive, computed } from 'vue'
|
import { onMounted, ref, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet, httpPost } from '@/utils/request'
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
import ApplyApiKeyDialog from './ApplyApiKeyDialog.vue'
|
import ApplyApiKeyDialog from './ApplyApiKeyDialog.vue'
|
||||||
|
|
||||||
interface OpenApp {
|
interface OpenApp {
|
||||||
@ -37,8 +39,6 @@ interface OpenApp {
|
|||||||
totalCallCount: number | string
|
totalCallCount: number | string
|
||||||
lastUsedAt?: string | null
|
lastUsedAt?: string | null
|
||||||
createDate: string
|
createDate: string
|
||||||
connectCode?: string
|
|
||||||
connectCodeExpire?: string | number
|
|
||||||
activatedAt?: string | null
|
activatedAt?: string | null
|
||||||
lastUsedIp?: string | null
|
lastUsedIp?: string | null
|
||||||
ipWhitelist?: string | null
|
ipWhitelist?: string | null
|
||||||
@ -88,11 +88,6 @@ const resetSecretVisible = ref(false)
|
|||||||
const resetSecretTarget = ref<OpenApp | null>(null)
|
const resetSecretTarget = ref<OpenApp | null>(null)
|
||||||
const newSecret = ref('')
|
const newSecret = ref('')
|
||||||
|
|
||||||
const resendQrVisible = ref(false)
|
|
||||||
const resendQrTarget = ref<OpenApp | null>(null)
|
|
||||||
const newConnectCode = ref('')
|
|
||||||
const newConnectCodeExpire = ref('')
|
|
||||||
|
|
||||||
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
|
||||||
@ -124,8 +119,6 @@ async function load() {
|
|||||||
totalCallCount: it.totalCallCount ?? 0,
|
totalCallCount: it.totalCallCount ?? 0,
|
||||||
lastUsedAt: it.lastUsedAt,
|
lastUsedAt: it.lastUsedAt,
|
||||||
createDate: it.createDate,
|
createDate: it.createDate,
|
||||||
connectCode: it.connectCode,
|
|
||||||
connectCodeExpire: it.connectCodeExpire,
|
|
||||||
activatedAt: it.activatedAt,
|
activatedAt: it.activatedAt,
|
||||||
lastUsedIp: it.lastUsedIp,
|
lastUsedIp: it.lastUsedIp,
|
||||||
ipWhitelist: it.ipWhitelist,
|
ipWhitelist: it.ipWhitelist,
|
||||||
@ -157,12 +150,14 @@ async function loadCustomers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ prop: 'customerName', label: '客户名', minWidth: 180 },
|
{ prop: 'customerName', label: '客户名', minWidth: 150 },
|
||||||
{ prop: 'appName', label: '应用名称', minWidth: 180 },
|
{ prop: 'appName', label: '应用名称', minWidth: 220 },
|
||||||
{ prop: 'appId', label: 'AppID', minWidth: 220 },
|
{ prop: 'appId', label: 'AppID', width: 240, showOverflowTooltip: true },
|
||||||
{ prop: 'status', label: '状态', width: 100, slot: 'status' as const },
|
{ prop: 'totalCallCount', label: '调用次数', width: 100, align: 'right' as const, formatter: (row: any) => row.totalCallCount ?? '--' },
|
||||||
|
{ prop: 'status', label: '状态', width: 90, slot: 'status' as const },
|
||||||
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' as const },
|
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' as const },
|
||||||
{ label: '操作', width: 280, slot: 'action' as const }
|
{ prop: 'lastUsedAt', label: '最后调用', width: 170, formatter: (row: any) => fmtDate(row.lastUsedAt) },
|
||||||
|
{ label: '操作', width: 220, align: 'center' as const, fixed: 'right' as const, slot: 'action' as const }
|
||||||
]
|
]
|
||||||
|
|
||||||
function onSearch() { page.value = 1; load() }
|
function onSearch() { page.value = 1; load() }
|
||||||
@ -206,7 +201,7 @@ async function revokeConfirm() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await httpPost('/sys/openapi/app/revoke', {
|
await httpPost('/sys/openapi/app/revoke', {
|
||||||
id: Number(revokeTarget.value.id),
|
id: String(revokeTarget.value.id),
|
||||||
reason: revokeReason.value
|
reason: revokeReason.value
|
||||||
})
|
})
|
||||||
ElMessage.success('撤销成功')
|
ElMessage.success('撤销成功')
|
||||||
@ -226,7 +221,7 @@ function deleteHandle(row: OpenApp) {
|
|||||||
async function deleteConfirm() {
|
async function deleteConfirm() {
|
||||||
if (!deleteTarget.value) return
|
if (!deleteTarget.value) return
|
||||||
try {
|
try {
|
||||||
await httpPost('/sys/openapi/app/delete', { id: Number(deleteTarget.value.id) })
|
await httpPost('/sys/openapi/app/delete', { id: String(deleteTarget.value.id) })
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
deleteVisible.value = false
|
deleteVisible.value = false
|
||||||
deleteTarget.value = null
|
deleteTarget.value = null
|
||||||
@ -253,29 +248,6 @@ async function resetSecretConfirm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resendQrHandle(row: OpenApp) {
|
|
||||||
resendQrTarget.value = row
|
|
||||||
newConnectCode.value = ''
|
|
||||||
newConnectCodeExpire.value = ''
|
|
||||||
resendQrVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resendQrConfirm() {
|
|
||||||
if (!resendQrTarget.value) return
|
|
||||||
try {
|
|
||||||
const data = await httpPost<{ connectCode: string; connectCodeExpire: string | number }>(
|
|
||||||
`/sys/openapi/app/resend-qr/${resendQrTarget.value.id}`
|
|
||||||
)
|
|
||||||
newConnectCode.value = data?.connectCode || ''
|
|
||||||
newConnectCodeExpire.value = data?.connectCodeExpire
|
|
||||||
? String(data.connectCodeExpire)
|
|
||||||
: ''
|
|
||||||
ElMessage.success('连接码已重发,旧码已作废')
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error(e?.msg || e?.message || '重发失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyText(text: string, label = '已复制') {
|
async function copyText(text: string, label = '已复制') {
|
||||||
// 优先用现代 Clipboard API (HTTPS / localhost 才允许)
|
// 优先用现代 Clipboard API (HTTPS / localhost 才允许)
|
||||||
try {
|
try {
|
||||||
@ -321,16 +293,10 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-openapi-app">
|
<div class="rr-openapi-app">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="应用列表" :sub="`共 ${total} 个集成应用`">
|
||||||
<div class="rr-page-titles">
|
<el-button @click="onReset">重置</el-button>
|
||||||
<h2 class="rr-page-title">应用列表</h2>
|
<el-button type="primary" @click="onApplyDialogOpen">+ 申请新 APIKey</el-button>
|
||||||
<p class="rr-page-sub">共 {{ total }} 个集成应用</p>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="onApplyDialogOpen">+ 申请新 APIKey</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-tabs-row">
|
<div class="rr-tabs-row">
|
||||||
<div class="rr-tabs">
|
<div class="rr-tabs">
|
||||||
@ -342,33 +308,19 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
@click="onStatusTab(t.value)"
|
@click="onStatusTab(t.value)"
|
||||||
>{{ t.label }}</div>
|
>{{ t.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rr-filters">
|
|
||||||
<el-input
|
|
||||||
v-model="filters.searchKey"
|
|
||||||
placeholder="搜索 app 名称 / app_id / 客户名"
|
|
||||||
clearable
|
|
||||||
class="rr-search-input"
|
|
||||||
:prefix-icon="'Search'"
|
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
|
||||||
<el-select
|
|
||||||
v-model="filters.customerCode"
|
|
||||||
placeholder="客户"
|
|
||||||
class="rr-customer-select"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="opt in customerOptions"
|
|
||||||
:key="opt.value"
|
|
||||||
:label="opt.label"
|
|
||||||
:value="opt.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-button class="rr-btn-secondary" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SearchBar
|
||||||
|
v-model:filters="filters"
|
||||||
|
:fields='[
|
||||||
|
{ key: "searchKey", label: "", type: "text", placeholder: "搜索 app 名称 / app_id / 客户名", clearable: true },
|
||||||
|
{ key: "customerCode", label: "客户", type: "select", placeholder: "客户", clearable: true,
|
||||||
|
options: customerOptions.map(o => ({ value: o.value, label: o.label })) }
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="load"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@ -389,35 +341,15 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
{{ fmtDate(row.createDate) }}
|
{{ fmtDate(row.createDate) }}
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<el-button text type="primary" size="small" @click="detailHandle(row)">详情</el-button>
|
<TableActions
|
||||||
<el-button
|
:row="row"
|
||||||
v-if="row.status === 0 || row.status === 1"
|
:actions="[
|
||||||
text
|
{ label: '详情', type: 'primary', onClick: detailHandle },
|
||||||
type="primary"
|
{ label: '重置密钥', type: 'warning', when: (r: any) => r.status === 0 || r.status === 1, onClick: resetSecretHandle },
|
||||||
size="small"
|
{ label: '撤销', type: 'danger', divided: true, when: (r: any) => r.status === 0 || r.status === 1, onClick: revokeHandle },
|
||||||
@click="resendQrHandle(row)"
|
{ label: '删除', type: 'danger', divided: true, when: (r: any) => r.status === 2, onClick: deleteHandle }
|
||||||
>重发二维码</el-button>
|
]"
|
||||||
<el-button
|
/>
|
||||||
v-if="row.status === 0 || row.status === 1"
|
|
||||||
text
|
|
||||||
type="warning"
|
|
||||||
size="small"
|
|
||||||
@click="resetSecretHandle(row)"
|
|
||||||
>重置密钥</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="row.status === 0 || row.status === 1"
|
|
||||||
text
|
|
||||||
size="small"
|
|
||||||
class="rr-danger-text"
|
|
||||||
@click="revokeHandle(row)"
|
|
||||||
>撤销</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="row.status === 2"
|
|
||||||
text
|
|
||||||
size="small"
|
|
||||||
class="rr-danger-text"
|
|
||||||
@click="deleteHandle(row)"
|
|
||||||
>删除</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
@ -430,16 +362,15 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
>
|
>
|
||||||
<el-descriptions v-if="currentRow" :column="1" border>
|
<el-descriptions v-if="currentRow" :column="1" border>
|
||||||
<el-descriptions-item label="AppID">{{ currentRow.appId }}</el-descriptions-item>
|
<el-descriptions-item label="AppID">{{ currentRow.appId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="MCP地址">
|
||||||
|
<code>{{ (currentRow as any).mcpEndpoint || '—' }}</code>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="应用名称">{{ currentRow.appName }}</el-descriptions-item>
|
<el-descriptions-item label="应用名称">{{ currentRow.appName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户名称">{{ currentRow.customerName || currentRow.customerCode || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="客户名称">{{ currentRow.customerName || currentRow.customerCode || '—' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户标识">{{ currentRow.customerCode }}</el-descriptions-item>
|
<el-descriptions-item label="客户标识">{{ currentRow.customerCode }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<span :class="['rr-badge', `rr-badge-${currentRow.status}`]">{{ STATUS_LABEL[currentRow.status] || currentRow.status }}</span>
|
<span :class="['rr-badge', `rr-badge-${currentRow.status}`]">{{ STATUS_LABEL[currentRow.status] || currentRow.status }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="ConnectCode">
|
|
||||||
<code class="rr-code-accent">{{ currentRow.connectCode || '—' }}</code>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="ConnectCode 过期">{{ fmtDate(currentRow.connectCodeExpire) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="权限范围">{{ currentRow.scopes }}</el-descriptions-item>
|
<el-descriptions-item label="权限范围">{{ currentRow.scopes }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="IP 白名单">{{ currentRow.ipWhitelist || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="IP 白名单">{{ currentRow.ipWhitelist || '—' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="激活时间">{{ currentRow.activatedAt || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="激活时间">{{ currentRow.activatedAt || '—' }}</el-descriptions-item>
|
||||||
@ -497,18 +428,17 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
<p style="margin-bottom: 6px; font-size: 13px; color: var(--ink-2);">
|
<p style="margin-bottom: 6px; font-size: 13px; color: var(--ink-2);">
|
||||||
新的 AppSecret: <span style="color: var(--ink-3); font-size: 12px;">(明文, 可直接选中复制)</span>
|
新的 AppSecret: <span style="color: var(--ink-3); font-size: 12px;">(明文, 可直接选中复制)</span>
|
||||||
</p>
|
</p>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="newSecret"
|
v-model="newSecret"
|
||||||
readonly
|
readonly
|
||||||
type="text"
|
type="text"
|
||||||
size="default"
|
size="default"
|
||||||
style="font-family: ui-monospace, monospace;"
|
style="flex: 1; font-family: ui-monospace, monospace;"
|
||||||
@focus="onSecretInputFocus"
|
@focus="onSecretInputFocus"
|
||||||
>
|
/>
|
||||||
<template #append>
|
<el-button type="primary" style="flex-shrink: 0;" @click="copyText(newSecret, 'AppSecret 已复制')">复制</el-button>
|
||||||
<el-button type="primary" @click="copyText(newSecret, 'AppSecret 已复制')">复制</el-button>
|
</div>
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button v-if="!newSecret" @click="resetSecretVisible = false">取消</el-button>
|
<el-button v-if="!newSecret" @click="resetSecretVisible = false">取消</el-button>
|
||||||
@ -517,73 +447,19 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 重发连接码(二维码)弹窗 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="resendQrVisible"
|
|
||||||
title="重发连接码"
|
|
||||||
width="560"
|
|
||||||
:style="{ borderRadius: '12px', overflow: 'hidden' }"
|
|
||||||
align-center
|
|
||||||
destroy-on-close
|
|
||||||
>
|
|
||||||
<el-alert v-if="!newConnectCode" type="warning" :closable="false" style="margin-bottom: 16px">
|
|
||||||
<p>重发后会生成新的 connectCode,旧码立即作废</p>
|
|
||||||
<p>新码有效期 2 小时,请尽快发给客户扫码激活</p>
|
|
||||||
</el-alert>
|
|
||||||
<el-alert v-if="newConnectCode" type="error" :closable="false" style="margin-bottom: 16px">
|
|
||||||
<p>新连接码仅显示一次!请立即复制并通过安全渠道发给客户</p>
|
|
||||||
<p>关闭弹窗后无法再次查看(只能再次重发)</p>
|
|
||||||
</el-alert>
|
|
||||||
<p v-if="resendQrTarget && !newConnectCode">
|
|
||||||
app: <code class="rr-code-accent">{{ resendQrTarget.appId }}</code>
|
|
||||||
({{ resendQrTarget.appName }})
|
|
||||||
</p>
|
|
||||||
<div v-if="newConnectCode" style="margin-top: 12px;">
|
|
||||||
<p style="margin-bottom: 6px; font-size: 13px; color: var(--el-text-color-regular);">
|
|
||||||
新的 ConnectCode: <span style="color: var(--el-text-color-secondary); font-size: 12px;">(明文,可直接选中复制)</span>
|
|
||||||
</p>
|
|
||||||
<el-input
|
|
||||||
v-model="newConnectCode"
|
|
||||||
readonly
|
|
||||||
type="text"
|
|
||||||
size="default"
|
|
||||||
style="font-family: ui-monospace, monospace;"
|
|
||||||
@focus="onSecretInputFocus"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<el-button type="primary" @click="copyText(newConnectCode, '连接码已复制')">复制</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<p v-if="newConnectCodeExpire" style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
|
||||||
过期时间: {{ fmtDate(newConnectCodeExpire) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button v-if="!newConnectCode" @click="resendQrVisible = false">取消</el-button>
|
|
||||||
<el-button v-if="!newConnectCode" type="primary" @click="resendQrConfirm">确认重发</el-button>
|
|
||||||
<el-button v-if="newConnectCode" type="primary" @click="resendQrVisible = false">我已复制,关闭</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 申请 APIKey 向导(2 步) -->
|
<!-- 申请 APIKey 向导(2 步) -->
|
||||||
<ApplyApiKeyDialog v-model="applyDialogVisible" @refresh="onApplySubmitted" />
|
<ApplyApiKeyDialog v-model="applyDialogVisible" @refresh="onApplySubmitted" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-openapi-app { max-width: 1400px; }
|
.rr-openapi-app { max-width: 100%; }
|
||||||
|
|
||||||
.rr-page-header {
|
/* PageHeader/SearchBar 接管 */
|
||||||
display: flex; align-items: flex-start; justify-content: space-between;
|
/* PageHeader/SearchBar 接管 */
|
||||||
margin-bottom: 20px;
|
/* PageHeader/SearchBar 接管 */
|
||||||
}
|
/* PageHeader/SearchBar 接管 */
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 4px; }
|
/* PageHeader/SearchBar 接管 */
|
||||||
.rr-page-title {
|
|
||||||
font-size: 22px; font-weight: 600; letter-spacing: -0.02em;
|
|
||||||
color: var(--ink); margin: 0;
|
|
||||||
}
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-page-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
|
||||||
|
|
||||||
.rr-list-card {
|
.rr-list-card {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
@ -610,9 +486,9 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
height: 2px; background: var(--brand); border-radius: 1px;
|
height: 2px; background: var(--brand); border-radius: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.rr-filters { display: flex; gap: 8px; flex-shrink: 0; }
|
/* PageHeader/SearchBar 接管 */
|
||||||
.rr-search-input { width: 280px; }
|
/* PageHeader/SearchBar 接管 */
|
||||||
.rr-customer-select { width: 160px; }
|
/* PageHeader/SearchBar 接管 */
|
||||||
|
|
||||||
/* 状态徽章(浅底色块 - 4 种状态) */
|
/* 状态徽章(浅底色块 - 4 种状态) */
|
||||||
.rr-badge {
|
.rr-badge {
|
||||||
@ -620,10 +496,10 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
font-size: 11.5px; font-weight: 500; line-height: 17.25px; height: 21.25px;
|
font-size: 11.5px; font-weight: 500; line-height: 17.25px; height: 21.25px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.rr-badge-0 { background: #FEF3C7; color: #D97706; } /* 待激活 amber */
|
.rr-badge-0 { background: var(--amber-soft); color: var(--amber); } /* 待激活 amber */
|
||||||
.rr-badge-1 { background: #D1FAE5; color: #059669; } /* 已激活 green */
|
.rr-badge-1 { background: var(--green-soft); color: var(--green); } /* 已激活 green */
|
||||||
.rr-badge-2 { background: #DBEAFE; color: #2563EB; } /* 已撤销 blue */
|
.rr-badge-2 { background: var(--brand-soft); color: var(--brand); } /* 已撤销 blue */
|
||||||
.rr-badge-3 { background: #E2E8F0; color: #64748B; } /* 已过期 gray */
|
.rr-badge-3 { background: var(--surface-2); color: var(--ink-3); } /* 已过期 gray */
|
||||||
|
|
||||||
/* 危险文字按钮(撤销/删除 - 老 vue 用 style color: #ff4d4f) */
|
/* 危险文字按钮(撤销/删除 - 老 vue 用 style color: #ff4d4f) */
|
||||||
:deep(.rr-danger-text) {
|
:deep(.rr-danger-text) {
|
||||||
@ -646,7 +522,7 @@ onMounted(() => { load(); loadCustomers() })
|
|||||||
:deep(.el-input__inner),
|
:deep(.el-input__inner),
|
||||||
:deep(.el-select__wrapper) {
|
:deep(.el-select__wrapper) {
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
background: #F8FAFC;
|
background: var(--surface-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 弹窗圆角 12px + 紧凑(套设计规范 §7.1 + §17 v3) */
|
/* 弹窗圆角 12px + 紧凑(套设计规范 §7.1 + §17 v3) */
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 订单详情 - 完整版 (W5.x.x+5 整改)
|
* F006 订单详情 - W10 A6 标准化
|
||||||
|
*
|
||||||
|
* W10 A6: 套 PageHeader (返回按钮 slot)
|
||||||
*
|
*
|
||||||
* 沿用老 vue order/order-detail.vue 1379 行完整功能:
|
* 沿用老 vue order/order-detail.vue 1379 行完整功能:
|
||||||
* - 4 个 tab: 订单信息 / 商品明细 / 物流信息 / 付款发票
|
* - 4 个 tab: 订单信息 / 商品明细 / 物流信息 / 付款发票
|
||||||
@ -19,6 +21,7 @@ import { ref, onMounted, watch, computed, reactive } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
interface OrderDetail {
|
interface OrderDetail {
|
||||||
id: string
|
id: string
|
||||||
@ -325,9 +328,9 @@ watch(orderId, load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-order-detail">
|
<div class="rr-order-detail">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="订单详情" :sub="detail?.orderCode || ''">
|
||||||
<el-button text @click="onBack">← 返回订单列表</el-button>
|
<el-button text @click="onBack">← 返回订单列表</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
|
|
||||||
<div v-if="loading" class="rr-loading">
|
<div v-if="loading" class="rr-loading">
|
||||||
<el-skeleton :rows="6" animated />
|
<el-skeleton :rows="6" animated />
|
||||||
@ -547,7 +550,6 @@ watch(orderId, load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-order-detail { max-width: 1400px; }
|
.rr-order-detail { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
||||||
.rr-card-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
.rr-card-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
||||||
.rr-card-title { font-size: 15px; font-weight: 600; color: var(--ink); margin: 0; }
|
.rr-card-title { font-size: 15px; font-weight: 600; color: var(--ink); margin: 0; }
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 订单管理列表 - 完整版 (W5.x.x+5 整改)
|
* F006 订单管理列表 - W9 A3 标准化示范
|
||||||
*
|
*
|
||||||
* 沿用老 vue order/order-good.vue 289 行 + order/serviceorder 154 行等:
|
* 沿用老 vue order/order-good.vue 289 行 + order/serviceorder 154 行等:
|
||||||
* - 搜索: 订单编号/合同编号/供应商/采购方/状态/创建时间范围
|
* - 搜索: 订单编号/合同编号/供应商/采购方/状态/创建时间范围
|
||||||
@ -9,12 +9,21 @@
|
|||||||
* - 操作: 详情/付款/发货/收货/取消/退款/导出
|
* - 操作: 详情/付款/发货/收货/取消/退款/导出
|
||||||
* - 批量导出
|
* - 批量导出
|
||||||
* - 通过 query.type 区分 good/service/group
|
* - 通过 query.type 区分 good/service/group
|
||||||
|
*
|
||||||
|
* 字段: id/orderNo/contractCode/enterpriseName/orderUserName/orderType/totalAmount/totalNum/shippedNum/status/createDate/payDate/payType/pickupType
|
||||||
|
* 端点: /order/ordergood/page 200 (按 type 区分 good/service/group)
|
||||||
|
*
|
||||||
|
* W9 A3: 用 PageHeader / SearchBar 组件替换硬编码, 用 DataTable type:'selection' 替换裸表
|
||||||
*/
|
*/
|
||||||
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 } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
id: string
|
id: string
|
||||||
@ -63,10 +72,37 @@ const FALLBACK: Order[] = [
|
|||||||
const list = ref<Order[]>([])
|
const list = ref<Order[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const limit = ref(10)
|
const limit = ref(10)
|
||||||
const error = ref<string | null>(null)
|
const filters = reactive({
|
||||||
|
orderNo: '',
|
||||||
|
contractCode: '',
|
||||||
|
enterpriseName: '',
|
||||||
|
status: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
})
|
||||||
const selection = ref<Order[]>([])
|
const selection = ref<Order[]>([])
|
||||||
|
const columns: any = computed(() => [
|
||||||
|
{ type: 'selection', label: '', width: 50 },
|
||||||
|
{ prop: 'orderNo', label: '订单号', width: 180 },
|
||||||
|
{ prop: 'contractCode', label: '合同号', width: 160, slot: 'contractCode' },
|
||||||
|
{ prop: 'enterpriseName', label: '供应商', minWidth: 180, slot: 'enterpriseName' },
|
||||||
|
{ prop: 'orderUserName', label: '采购方', minWidth: 160, slot: 'orderUserName' },
|
||||||
|
{ prop: 'totalAmount', label: '金额', width: 130, align: 'right', slot: 'amount' },
|
||||||
|
{ prop: 'totalNum', label: '数量', width: 80, align: 'center' },
|
||||||
|
{ prop: 'shippedNum', label: '已发', width: 80, align: 'center' },
|
||||||
|
{ prop: 'status', label: '状态', width: 110, align: 'center', slot: 'status' },
|
||||||
|
{ prop: 'createDate', label: '下单时间', width: 170, slot: 'time' },
|
||||||
|
{ label: '操作', width: 140, align: 'center', slot: 'action' }
|
||||||
|
])
|
||||||
|
const tabItems = computed(() => STATUS_TABS.map(t => ({
|
||||||
|
key: t.value,
|
||||||
|
label: t.label,
|
||||||
|
count: t.value === '' ? stats.all : (stats[Number(t.value)] || 0)
|
||||||
|
})))
|
||||||
|
const stats = reactive<Record<number|string, number>>({ 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, all: 0 })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -80,161 +116,184 @@ const listType = computed(() => {
|
|||||||
return 'good'
|
return 'good'
|
||||||
})
|
})
|
||||||
|
|
||||||
const filters = reactive({
|
// W12.2 修正: 后端 OpenApiOrderController 只暴露 /openapi/order/list 一个端点,
|
||||||
orderNo: '',
|
// 旧路径 (/order/ordergood/page 等) 404。所有 5 种订单类型都走统一端点。
|
||||||
contractCode: '',
|
const endpoint = computed(() => '/sys/openapi/order/list')
|
||||||
enterpriseName: '',
|
|
||||||
status: '',
|
|
||||||
startDate: '',
|
|
||||||
endDate: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const typeLabel = computed(() => {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
good: '商品订单', service: '服务订单', groupsupplier: '商品拼团订单', groupservice: '服务拼团订单', aftersale: '售后订单', commission: '分佣统计'
|
|
||||||
}
|
|
||||||
return map[listType.value] || '订单管理'
|
|
||||||
})
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await new Promise(r => setTimeout(r, 200))
|
// W12.3 修复: httpGet() 已解开 res.data.data, 所以 r 直接是 {total, list, page, limit}
|
||||||
let rows = [...FALLBACK]
|
const r: any = await httpGet(endpoint.value, { page: page.value, limit: limit.value })
|
||||||
if (filters.orderNo) rows = rows.filter(r => r.orderNo.includes(filters.orderNo))
|
const respList: any[] = Array.isArray(r?.list) ? r.list : []
|
||||||
if (filters.contractCode) rows = rows.filter(r => r.contractCode.includes(filters.contractCode))
|
const rows: Order[] = respList.map((it: any) => ({
|
||||||
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
|
id: it.orderId || '',
|
||||||
if (filters.status) rows = rows.filter(r => String(r.status) === filters.status)
|
orderNo: it.orderId || '',
|
||||||
|
contractCode: '',
|
||||||
|
enterpriseName: '',
|
||||||
|
orderUserName: '',
|
||||||
|
orderType: 1,
|
||||||
|
totalAmount: Number(it.totalAmount ?? 0),
|
||||||
|
totalNum: Number(it.goodsQuantity ?? 0),
|
||||||
|
shippedNum: 0,
|
||||||
|
status: Number(it.status ?? 0),
|
||||||
|
createDate: it.createDate ? String(it.createDate) : '',
|
||||||
|
payDate: null,
|
||||||
|
payType: 0,
|
||||||
|
pickupType: 1,
|
||||||
|
raw: it
|
||||||
|
}))
|
||||||
list.value = rows
|
list.value = rows
|
||||||
total.value = rows.length
|
// 后端 total 是字符串 "1356", parseInt 兜底
|
||||||
|
const t = r?.total
|
||||||
|
total.value = typeof t === 'string' ? parseInt(t, 10) : (Number(t) || 0)
|
||||||
|
// 各 status 计数
|
||||||
|
Object.keys(stats).forEach(k => (stats as any)[k] = 0)
|
||||||
|
stats.all = rows.length
|
||||||
|
rows.forEach(r => { stats[r.status] = ((stats[r.status] as number) || 0) + 1 })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.msg || e?.message || '加载失败'
|
error.value = e?.msg || e?.message || '加载失败'
|
||||||
list.value = FALLBACK
|
list.value = FALLBACK
|
||||||
total.value = FALLBACK.length
|
total.value = FALLBACK.length
|
||||||
} finally { loading.value = false }
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSearch() { page.value = 1; load() }
|
function onSearch() { page.value = 1; load() }
|
||||||
function onReset() { Object.assign(filters, { orderNo: '', contractCode: '', enterpriseName: '', status: '', startDate: '', endDate: '' }); load() }
|
function fmtDate(ts?: string | number | null) {
|
||||||
|
if (!ts) return '--'
|
||||||
|
let n: number
|
||||||
|
if (typeof ts === 'string') {
|
||||||
|
n = /^\d+$/.test(ts) ? Number(ts) : Date.parse(ts)
|
||||||
|
} else {
|
||||||
|
n = ts
|
||||||
|
}
|
||||||
|
if (!n || isNaN(n)) return String(ts)
|
||||||
|
const d = new Date(n)
|
||||||
|
if (isNaN(d.getTime())) return String(ts)
|
||||||
|
const pad = (x: number) => String(x).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReset() {
|
||||||
|
filters.orderNo = ''
|
||||||
|
filters.contractCode = ''
|
||||||
|
filters.enterpriseName = ''
|
||||||
|
filters.status = ''
|
||||||
|
filters.startDate = ''
|
||||||
|
filters.endDate = ''
|
||||||
|
load()
|
||||||
|
}
|
||||||
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; load() }
|
||||||
function onStatusTab(v: string) { filters.status = v; load() }
|
|
||||||
function onSelectionChange(rows: Order[]) { selection.value = rows }
|
function onSelectionChange(rows: Order[]) { selection.value = rows }
|
||||||
function onRowClick(row: Order) { router.push(`/order/${row.id}`) }
|
|
||||||
|
function onStatusTabChange(status: string) { filters.status = status; onSearch() }
|
||||||
function onViewDetail(row: Order) { router.push(`/order/${row.id}`) }
|
function onViewDetail(row: Order) { router.push(`/order/${row.id}`) }
|
||||||
function onPay(row: Order) { ElMessage.success(`付款 (mock) ${row.orderNo}`) }
|
function onPay(row: Order) { ElMessage.success(`付款 ${row.orderNo} (mock)`); load() }
|
||||||
function onShip(row: Order) { ElMessage.success(`发货 (mock) ${row.orderNo}`) }
|
function onShip(row: Order) { ElMessage.success(`发货 ${row.orderNo} (mock)`); load() }
|
||||||
function onConfirmReceipt(row: Order) { ElMessage.success(`确认收货 (mock) ${row.orderNo}`) }
|
function onConfirmReceipt(row: Order) { ElMessage.success(`确认收货 ${row.orderNo} (mock)`); load() }
|
||||||
function onCancel(row: Order) { ElMessage.info(`取消 (mock) ${row.orderNo}`) }
|
function onRefund(row: Order) { ElMessage.success(`退款 ${row.orderNo} (mock)`); load() }
|
||||||
function onRefund(row: Order) { ElMessage.info(`退款 (mock) ${row.orderNo}`) }
|
function onCancel(row: Order) { ElMessage.success(`取消 ${row.orderNo} (mock)`); load() }
|
||||||
function onExport() { ElMessage.success('导出订单 (mock)') }
|
function onPrint(row: Order) { ElMessage.info(`打印 ${row.orderNo} (mock)`) }
|
||||||
function onExportSelected() { if (!selection.value.length) { ElMessage.warning('请先选择订单'); return } ElMessage.success(`导出 ${selection.value.length} 个订单 (mock)`) }
|
function onExportSelected() {
|
||||||
function onPrint(row: Order) { ElMessage.info(`打印 (mock) ${row.orderNo}`) }
|
if (!selection.value.length) { ElMessage.warning('请先选择订单'); return }
|
||||||
|
ElMessage.success(`导出 ${selection.value.length} 个订单 (mock)`)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="订单管理" :sub="`共 ${total} 条订单${selection.length ? `, 已选 ${selection.length}` : ''}`">
|
||||||
<div>
|
<el-button v-if="selection.length > 0" type="primary" @click="onExportSelected">
|
||||||
<h2 class="rr-page-title">{{ typeLabel }}</h2>
|
导出选中 ({{ selection.length }})
|
||||||
<p class="rr-page-sub">共 {{ total }} 条订单</p>
|
</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onExport">导出</el-button>
|
|
||||||
<el-button v-if="selection.length > 0" type="primary" @click="onExportSelected">导出选中 ({{ selection.length }})</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索区 (沿用老 vue: 订单号/合同号/供应商/状态/时间) -->
|
<ListTabs v-model="filters.status" :items="tabItems" @change="onStatusTabChange" />
|
||||||
<div class="rr-card rr-search">
|
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
|
||||||
<el-form-item label="订单号">
|
|
||||||
<el-input v-model="filters.orderNo" placeholder="订单号" clearable style="width: 180px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="合同号">
|
|
||||||
<el-input v-model="filters.contractCode" placeholder="合同号" clearable style="width: 180px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="供应商">
|
|
||||||
<el-input v-model="filters.enterpriseName" placeholder="供应商" clearable style="width: 180px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="创建时间">
|
|
||||||
<el-date-picker v-model="filters.startDate" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" placeholder="开始" clearable style="width: 170px" />
|
|
||||||
<span style="margin: 0 8px">至</span>
|
|
||||||
<el-date-picker v-model="filters.endDate" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" placeholder="结束" clearable style="width: 170px" />
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- 状态 Tab -->
|
<SearchBar
|
||||||
<div class="rr-tabs-row">
|
v-model:filters="filters"
|
||||||
<div class="rr-tabs">
|
:fields="[
|
||||||
<div v-for="t in STATUS_TABS" :key="t.value" class="rr-tab" :class="{ active: filters.status === t.value }" @click="onStatusTab(t.value)">{{ t.label }}</div>
|
{ key: 'orderNo', label: '订单号', placeholder: '请输入订单号' },
|
||||||
</div>
|
{ key: 'contractCode', label: '合同号', placeholder: '请输入合同号' },
|
||||||
</div>
|
{ key: 'enterpriseName', label: '供应商', placeholder: '请输入供应商名称' },
|
||||||
|
{ key: 'startDate', label: '开始日期', type: 'date', placeholder: '选择开始日期' },
|
||||||
|
{ key: 'endDate', label: '结束日期', type: 'date', placeholder: '选择结束日期' }
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<DataTable
|
||||||
<div class="rr-card">
|
:columns="columns"
|
||||||
<el-table :data="list" border style="width: 100%" v-loading="loading" :empty-text="error || '暂无订单'" @selection-change="onSelectionChange" @row-click="onRowClick" row-class-name="rr-clickable">
|
:data="list"
|
||||||
<el-table-column type="selection" width="50" align="center" @selectable="(row: any) => row.status !== 7" />
|
:loading="loading"
|
||||||
<el-table-column type="index" label="#" width="50" align="center" />
|
:total="total"
|
||||||
<el-table-column prop="orderNo" label="订单号" width="180" />
|
:page="page"
|
||||||
<el-table-column prop="contractCode" label="合同号" width="160" show-overflow-tooltip />
|
:limit="limit"
|
||||||
<el-table-column prop="enterpriseName" label="供应商" min-width="180" show-overflow-tooltip />
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
<el-table-column prop="orderUserName" label="采购方" min-width="160" show-overflow-tooltip />
|
:show-pagination="true"
|
||||||
<el-table-column label="金额" width="120" align="right">
|
:error="error"
|
||||||
<template #default="{ row }">¥ {{ row.totalAmount.toLocaleString() }}</template>
|
empty-text="暂无订单"
|
||||||
</el-table-column>
|
:borderless="true"
|
||||||
<el-table-column label="数量" prop="totalNum" width="80" align="center" />
|
row-key="id"
|
||||||
<el-table-column label="已发" prop="shippedNum" width="80" align="center" />
|
@page-change="onPageChange"
|
||||||
<el-table-column label="状态" width="100" align="center">
|
@limit-change="onLimitChange"
|
||||||
<template #default="{ row }">
|
@selection-change="onSelectionChange"
|
||||||
<el-tag :type="(STATUS_LABEL[row.status] === '已完成' ? 'success' : STATUS_LABEL[row.status] === '已取消' ? 'info' : 'primary') as any" size="small" effect="light">
|
>
|
||||||
|
<template #contractCode="{ row }">
|
||||||
|
<span v-if="row.contractCode">{{ row.contractCode }}</span>
|
||||||
|
<span v-else class="rr-muted">--</span>
|
||||||
|
</template>
|
||||||
|
<template #enterpriseName="{ row }">
|
||||||
|
<span v-if="row.enterpriseName">{{ row.enterpriseName }}</span>
|
||||||
|
<span v-else class="rr-muted">--</span>
|
||||||
|
</template>
|
||||||
|
<template #orderUserName="{ row }">
|
||||||
|
<span v-if="row.orderUserName">{{ row.orderUserName }}</span>
|
||||||
|
<span v-else class="rr-muted">--</span>
|
||||||
|
</template>
|
||||||
|
<template #time="{ row }">
|
||||||
|
<span class="rr-time">{{ fmtDate(row.createDate) }}</span>
|
||||||
|
</template>
|
||||||
|
<template #amount="{ row }">
|
||||||
|
<span class="rr-amount">¥ {{ row.totalAmount.toLocaleString() }}</span>
|
||||||
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="(STATUS_LABEL[row.status] === '已完成' ? 'success' : STATUS_LABEL[row.status] === '已取消' ? 'info' : 'primary') as any"
|
||||||
|
size="small"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
{{ STATUS_LABEL[row.status] || `状态${row.status}` }}
|
{{ STATUS_LABEL[row.status] || `状态${row.status}` }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
<template #action="{ row }">
|
||||||
<el-table-column label="下单时间" width="170">
|
<TableActions
|
||||||
<template #default="{ row }">{{ row.createDate }}</template>
|
:row="row"
|
||||||
</el-table-column>
|
:actions="[
|
||||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
{ label: '详情', type: 'primary', onClick: onViewDetail },
|
||||||
<template #default="{ row }">
|
{ label: '付款', type: 'primary', when: (r: any) => r.status === 1, onClick: onPay },
|
||||||
<el-button text type="primary" size="small" @click.stop="onViewDetail(row)">详情</el-button>
|
{ label: '发货', type: 'primary', when: (r: any) => r.status === 2, onClick: onShip },
|
||||||
<el-button v-if="row.status === 1" text type="primary" size="small" @click.stop="onPay(row)">付款</el-button>
|
{ label: '收货', type: 'primary', when: (r: any) => r.status === 3, onClick: onConfirmReceipt },
|
||||||
<el-button v-if="row.status === 2" text type="primary" size="small" @click.stop="onShip(row)">发货</el-button>
|
{ label: '退款', type: 'warning', when: (r: any) => r.status === 4, onClick: onRefund },
|
||||||
<el-button v-if="row.status === 3" text type="primary" size="small" @click.stop="onConfirmReceipt(row)">收货</el-button>
|
{ label: '取消', type: 'danger', when: (r: any) => [1,2].includes(r.status), onClick: onCancel },
|
||||||
<el-button v-if="[1,2,3,4].includes(row.status)" text type="warning" size="small" @click.stop="onRefund(row)">退款</el-button>
|
{ label: '打印', type: 'primary', onClick: onPrint }
|
||||||
<el-button v-if="[1,2].includes(row.status)" text type="danger" size="small" @click.stop="onCancel(row)">取消</el-button>
|
]"
|
||||||
<el-button text size="small" @click.stop="onPrint(row)">打印</el-button>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</DataTable>
|
||||||
</el-table>
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1500px; }
|
.rr-page { max-width: 1500px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
.rr-muted { color: var(--ink-3); }
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
.rr-amount { font-weight: 600; color: var(--primary); }
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.rr-tabs-row { margin-bottom: 16px; }
|
|
||||||
.rr-tabs { display: flex; gap: 4px; padding: 4px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); width: fit-content; flex-wrap: wrap; }
|
|
||||||
.rr-tab { padding: 6px 16px; border-radius: var(--radius); font-size: 13px; color: var(--ink-2); cursor: pointer; }
|
|
||||||
.rr-tab.active { color: white; background: var(--primary); font-weight: 500; }
|
|
||||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
|
||||||
:deep(.rr-clickable) { cursor: pointer; }
|
|
||||||
:deep(.rr-clickable:hover > td) { background: var(--bg-soft) !important; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
220
src/modules/order/aftersale/AftersaleListView.vue
Normal file
220
src/modules/order/aftersale/AftersaleListView.vue
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 实物订单售后 - W15.5
|
||||||
|
*
|
||||||
|
* 沿用老 vue order/orderafter.vue 字段:
|
||||||
|
* afterSalesOrderId / shopType / afterSaleType / enterpriseName / name / title
|
||||||
|
* returnQuantity / totalAmount / requestTime / status (1=发起 2=处理中 3=同意 4=不同意 5=寄回 6=完成 7=取消)
|
||||||
|
*
|
||||||
|
* 端点: /order/aftersale/page 探测; 404 → FALLBACK seed (14 条)
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
|
interface AftersaleRow {
|
||||||
|
afterSalesOrderId: string
|
||||||
|
shopType: number
|
||||||
|
afterSaleType: number
|
||||||
|
enterpriseName: string
|
||||||
|
name: string
|
||||||
|
title: string
|
||||||
|
returnQuantity: number
|
||||||
|
totalAmount: number
|
||||||
|
requestTime: string
|
||||||
|
status: number
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHOP_LABEL: Record<number, string> = { 1: '实物商品', 2: '服务商品', 3: '拼团商品', 4: '虚拟商品' }
|
||||||
|
const TYPE_LABEL: Record<number, string> = { 1: '仅退款', 2: '退货退款', 3: '换货', 4: '维修' }
|
||||||
|
const STATUS_LABEL: Record<number, string> = { 1: '发起申请', 2: '处理中', 3: '同意退/换', 4: '不同意', 5: '货物寄回', 6: '完成', 7: '取消' }
|
||||||
|
const STATUS_TYPE: Record<number, string> = { 1:'warning', 2:'primary', 3:'success', 4:'danger', 5:'info', 6:'success', 7:'info' }
|
||||||
|
|
||||||
|
const ENDPOINTS = ['/order/orderAfterSale/shopAfterSalePage', '/order/orderAfterSale/supplierPageList']
|
||||||
|
|
||||||
|
const FALLBACK: AftersaleRow[] = [
|
||||||
|
{ afterSalesOrderId:'AS20260701001', shopType:1, afterSaleType:2, enterpriseName:'坚美建筑材料有限公司', name:'张老板', title:'国标槽钢 10# 6m 热镀锌', returnQuantity:5, totalAmount:3450.00, requestTime:'2026-07-01 14:20:00', status:3, reason:'型号发错' },
|
||||||
|
{ afterSalesOrderId:'AS20260701002', shopType:1, afterSaleType:1, enterpriseName:'华建门窗厂', name:'李工', title:'断桥铝平开窗 60系列', returnQuantity:2, totalAmount:1800.00, requestTime:'2026-07-02 09:18:42', status:6, reason:'协商退款' },
|
||||||
|
{ afterSalesOrderId:'AS20260702001', shopType:1, afterSaleType:2, enterpriseName:'融鑫管业', name:'王经理', title:'PPR给水管 dn25 4m', returnQuantity:20, totalAmount:1260.00, requestTime:'2026-07-02 16:32:11', status:2, reason:'包装破损' },
|
||||||
|
{ afterSalesOrderId:'AS20260702002', shopType:1, afterSaleType:4, enterpriseName:'东方五金机械', name:'赵师傅', title:'电锤 GBH 2-28 800W', returnQuantity:1, totalAmount:680.00, requestTime:'2026-07-03 11:42:30', status:4, reason:'已过维保期' },
|
||||||
|
{ afterSalesOrderId:'AS20260703001', shopType:1, afterSaleType:3, enterpriseName:'美洁涂料', name:'陈女士', title:'环保内墙乳胶漆 5L', returnQuantity:4, totalAmount:920.00, requestTime:'2026-07-03 13:28:00', status:5, reason:'颜色偏差' },
|
||||||
|
{ afterSalesOrderId:'AS20260703002', shopType:1, afterSaleType:2, enterpriseName:'万家灯具', name:'周总', title:'LED投光灯 200W IP65', returnQuantity:3, totalAmount:1080.00, requestTime:'2026-07-04 08:15:55', status:1, reason:'不亮灯' },
|
||||||
|
{ afterSalesOrderId:'AS20260704001', shopType:2, afterSaleType:1, enterpriseName:'顺达物流', name:'吴先生', title:'同城配送服务 (50km内)', returnQuantity:1, totalAmount:280.00, requestTime:'2026-07-04 16:48:22', status:7, reason:'客户主动取消' },
|
||||||
|
{ afterSalesOrderId:'AS20260704002', shopType:1, afterSaleType:2, enterpriseName:'华建门窗厂', name:'林总', title:'铝合金推拉门 90系列', returnQuantity:1, totalAmount:2880.00, requestTime:'2026-07-05 10:14:42', status:3, reason:'尺寸不符' },
|
||||||
|
{ afterSalesOrderId:'AS20260705001', shopType:3, afterSaleType:1, enterpriseName:'众成厨具', name:'黄总', title:'不锈钢汤锅 30cm', returnQuantity:1, totalAmount:128.00, requestTime:'2026-07-05 14:30:00', status:6, reason:'拼团失败退款' },
|
||||||
|
{ afterSalesOrderId:'AS20260705002', shopType:1, afterSaleType:4, enterpriseName:'东方五金机械', name:'何师傅', title:'角磨机 100mm 720W', returnQuantity:1, totalAmount:380.00, requestTime:'2026-07-06 09:25:00', status:2, reason:'电机异响' },
|
||||||
|
{ afterSalesOrderId:'AS20260706001', shopType:1, afterSaleType:2, enterpriseName:'坚美建筑材料', name:'钱老板', title:'国标角钢 50×5 6m', returnQuantity:8, totalAmount:2240.00, requestTime:'2026-07-06 11:02:18', status:3, reason:'厚度偏差' },
|
||||||
|
{ afterSalesOrderId:'AS20260706002', shopType:1, afterSaleType:1, enterpriseName:'美洁涂料', name:'孙总', title:'环保外墙腻子粉 25kg', returnQuantity:6, totalAmount:780.00, requestTime:'2026-07-06 15:48:32', status:1, reason:'施工后剩余' },
|
||||||
|
{ afterSalesOrderId:'AS20260706003', shopType:1, afterSaleType:2, enterpriseName:'融鑫管业', name:'李经理', title:'PE给水管 dn32 6m', returnQuantity:12, totalAmount:960.00, requestTime:'2026-07-06 17:33:21', status:2, reason:'现场清点异常' },
|
||||||
|
{ afterSalesOrderId:'AS20260706004', shopType:2, afterSaleType:1, enterpriseName:'顺达物流', name:'郑总', title:'异地整车运输', returnQuantity:1, totalAmount:1680.00, requestTime:'2026-07-06 18:55:08', status:1, reason:'时效不达标' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const STATUS_TABS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '1', label: '发起申请' },
|
||||||
|
{ value: '2', label: '处理中' },
|
||||||
|
{ value: '3', label: '已同意' },
|
||||||
|
{ value: '4', label: '已拒绝' },
|
||||||
|
{ value: '5', label: '货物寄回' },
|
||||||
|
{ value: '6', label: '已完成' },
|
||||||
|
{ value: '7', label: '已取消' }
|
||||||
|
]
|
||||||
|
|
||||||
|
type Col = {
|
||||||
|
prop?: string; label: string; width?: number; slot?: string; fixed?: 'right' | true; type?: 'status' | 'text' | 'selection'
|
||||||
|
}
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ prop: 'afterSalesOrderId', label: '售后订单号', width: 180, fixed: true, slot: 'orderId' },
|
||||||
|
{ type: 'status', prop: 'shopType', label: '商品类型', width: 110 },
|
||||||
|
{ type: 'status', prop: 'afterSaleType', label: '售后类型', width: 100 },
|
||||||
|
{ prop: 'enterpriseName', label: '供应商信息', width: 200 },
|
||||||
|
{ prop: 'name', label: '用户信息', width: 120 },
|
||||||
|
{ prop: 'title', label: '商品信息', width: 240, slot: 'title' },
|
||||||
|
{ prop: 'returnQuantity', label: '售后数量', width: 90 },
|
||||||
|
{ prop: 'totalAmount', label: '退款金额', width: 110 },
|
||||||
|
{ prop: 'requestTime', label: '创建时间', width: 160, slot: 'time' },
|
||||||
|
{ type: 'status', prop: 'status', label: '订单状态', width: 110 },
|
||||||
|
{ prop: 'reason', label: '售后原因', width: 200, slot: 'reason' },
|
||||||
|
{ prop: 'actions', label: '操作', width: 220, fixed: 'right', slot: 'actions' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const list = ref<AftersaleRow[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
const limit = ref(10)
|
||||||
|
const filterKw = ref('')
|
||||||
|
const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api' | 'mock' | 'loading'>('loading')
|
||||||
|
const apiError = ref<string>('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const params: any = { page: page.value, limit: limit.value }
|
||||||
|
const data: any = await httpGet<any>(ep, params)
|
||||||
|
const rows: AftersaleRow[] = (data?.list || []).map((it: any) => ({ ...it }))
|
||||||
|
list.value = rows
|
||||||
|
total.value = Number(data?.total || rows.length)
|
||||||
|
dataSource.value = 'api'
|
||||||
|
apiError.value = ''
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]
|
||||||
|
total.value = FALLBACK.length
|
||||||
|
dataSource.value = 'mock'
|
||||||
|
apiError.value = '后端 API 暂未实装, 已加载前端示例数据'
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase()
|
||||||
|
let rows = list.value
|
||||||
|
if (statusTab.value) rows = rows.filter(r => String(r.status) === statusTab.value)
|
||||||
|
if (kw) rows = rows.filter(r =>
|
||||||
|
String(r.afterSalesOrderId).toLowerCase().includes(kw) ||
|
||||||
|
String(r.enterpriseName).toLowerCase().includes(kw) ||
|
||||||
|
String(r.name).toLowerCase().includes(kw) ||
|
||||||
|
String(r.title).toLowerCase().includes(kw)
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [
|
||||||
|
{ key: 'afterSalesOrderId', label: '售后订单号', type: 'text' },
|
||||||
|
{ key: 'enterpriseName', label: '供应商', type: 'text' },
|
||||||
|
{ key: 'title', label: '商品名', type: 'text' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() {
|
||||||
|
filterKw.value = String(filters.afterSalesOrderId || filters.enterpriseName || filters.title || '')
|
||||||
|
page.value = 1
|
||||||
|
}
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
|
||||||
|
async function onAgree(row: AftersaleRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认通过售后申请「${row.afterSalesOrderId}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
row.status = 3
|
||||||
|
ElMessage.success('已通过 (前端示例)')
|
||||||
|
}
|
||||||
|
async function onReject(row: AftersaleRow) {
|
||||||
|
try {
|
||||||
|
const { value } = await ElMessageBox.prompt('请填写拒绝原因', '拒绝售后', { inputPlaceholder: '原因', confirmButtonText: '拒绝', cancelButtonText: '取消' })
|
||||||
|
row.status = 4
|
||||||
|
row.reason = (value || '已拒绝').trim()
|
||||||
|
ElMessage.success('已拒绝 (前端示例)')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
async function onComplete(row: AftersaleRow) {
|
||||||
|
row.status = 6
|
||||||
|
ElMessage.success('已标记完成')
|
||||||
|
}
|
||||||
|
function onView(row: AftersaleRow) {
|
||||||
|
ElMessage.info(`打开售后详情 ${row.afterSalesOrderId}`)
|
||||||
|
}
|
||||||
|
function onExport() {
|
||||||
|
ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="实物订单售后" sub="实物商品的退/换/修售后流程审核" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_TABS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:columns="columns"
|
||||||
|
:data="filtered"
|
||||||
|
:loading="loading"
|
||||||
|
:page="page"
|
||||||
|
:limit="limit"
|
||||||
|
:total="total"
|
||||||
|
row-key="afterSalesOrderId"
|
||||||
|
@page-change="(p: number) => { page = p; load() }"
|
||||||
|
@limit-change="(l: number) => { limit = l; page = 1; load() }"
|
||||||
|
>
|
||||||
|
<template #orderId="{ row }">
|
||||||
|
<el-link type="primary" :underline="false" @click="onView(row)">{{ row.afterSalesOrderId }}</el-link>
|
||||||
|
</template>
|
||||||
|
<template #title="{ row }">
|
||||||
|
<span :class="{ 'rr-muted': !row.title }">{{ row.title || '--' }}</span>
|
||||||
|
</template>
|
||||||
|
<template #time="{ row }">{{ row.requestTime }}</template>
|
||||||
|
<template #reason="{ row }">
|
||||||
|
<span :class="{ 'rr-muted': !row.reason }">{{ row.reason || '--' }}</span>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<template v-if="row.status === 1 || row.status === 2">
|
||||||
|
<el-button type="success" link @click="onAgree(row)">同意</el-button>
|
||||||
|
<el-button type="danger" link @click="onReject(row)">拒绝</el-button>
|
||||||
|
</template>
|
||||||
|
<el-button v-if="row.status === 5" type="success" link @click="onComplete(row)">确认收货完成</el-button>
|
||||||
|
<el-button type="primary" link @click="onView(row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
.rr-muted { color: var(--ink-muted); }
|
||||||
|
</style>
|
||||||
214
src/modules/order/payment/PaymentCallbackListView.vue
Normal file
214
src/modules/order/payment/PaymentCallbackListView.vue
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 订单支付回调列表 - W15.2
|
||||||
|
*
|
||||||
|
* 沿用老 vue order/orderpay.vue 字段:
|
||||||
|
* - 选择 + id + orderUserId + userType + orderId + goodDetailId
|
||||||
|
* - orderGoodId + orderBatchId + payStatus + sum + couponMoney + couponId
|
||||||
|
* - freight + payMoney + unitPrice + totalNum + payDate + isDel + pickupType
|
||||||
|
*
|
||||||
|
* 端点: /order/orderpay/page 探测 (按 5.18 4 URL + 老 vue 字段口径)
|
||||||
|
* 端点 404 → 用 FALLBACK 前端 seed 数据 (15 条)
|
||||||
|
*
|
||||||
|
* 状态: payStatus 1=预付款 2=发货款 3=到货款 4=安装款 5=结算款 6=质保金 7=已结束
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
|
interface PaymentRow {
|
||||||
|
id: string
|
||||||
|
orderUserId: string
|
||||||
|
userType: number
|
||||||
|
orderId: string
|
||||||
|
goodDetailId: string
|
||||||
|
orderGoodId: string
|
||||||
|
orderBatchId: string
|
||||||
|
payStatus: number
|
||||||
|
sum: number
|
||||||
|
couponMoney: number
|
||||||
|
couponId: string
|
||||||
|
freight: number
|
||||||
|
payMoney: number
|
||||||
|
unitPrice: number
|
||||||
|
totalNum: number
|
||||||
|
payDate: string
|
||||||
|
isDel: number
|
||||||
|
pickupType: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAY_STATUS_LABEL: Record<number, string> = {
|
||||||
|
1: '预付款', 2: '发货款', 3: '到货款', 4: '安装款', 5: '结算款', 6: '质保金', 7: '已结束'
|
||||||
|
}
|
||||||
|
|
||||||
|
const PICKUP_LABEL: Record<number, string> = { 1: '商家配送', 2: '买家自提' }
|
||||||
|
const USER_TYPE_LABEL: Record<number, string> = { 1: 'C端用户', 2: '供应链' }
|
||||||
|
|
||||||
|
// 端点探测 (W5.18 教训: 4 URL 试探 + 老 vue 字段口径, 找不到走 FALLBACK seed)
|
||||||
|
const ENDPOINTS = ['/order/orderpay/page', '/order/orderpay/list', '/order/orderpay/search', '/order/orderpay/queryPage']
|
||||||
|
|
||||||
|
const FALLBACK: PaymentRow[] = [
|
||||||
|
{ id:'OP001', orderUserId:'u10086', userType:1, orderId:'O001', goodDetailId:'GD101', orderGoodId:'OG201', orderBatchId:'OB301', payStatus:7, sum:12800.00, couponMoney:300.00, couponId:'C202501', freight:0, payMoney:12500.00, unitPrice:1280.00, totalNum:10, payDate:'2026-07-01 14:32:11', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP002', orderUserId:'u10112', userType:2, orderId:'O002', goodDetailId:'GD102', orderGoodId:'OG202', orderBatchId:'OB302', payStatus:3, sum:5600.00, couponMoney:0, couponId:'', freight:80.00, payMoney:5680.00, unitPrice:560.00, totalNum:10, payDate:'2026-07-02 09:18:42', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP003', orderUserId:'u10133', userType:1, orderId:'O003', goodDetailId:'GD103', orderGoodId:'OG203', orderBatchId:'OB303', payStatus:5, sum:8800.00, couponMoney:200.00, couponId:'C202503', freight:100.00, payMoney:8700.00, unitPrice:880.00, totalNum:10, payDate:'2026-06-28 16:45:00', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP004', orderUserId:'u10144', userType:1, orderId:'O004', goodDetailId:'GD104', orderGoodId:'OG204', orderBatchId:'OB304', payStatus:7, sum:2200.00, couponMoney:0, couponId:'', freight:0, payMoney:2200.00, unitPrice:220.00, totalNum:10, payDate:'2026-07-03 11:02:18', isDel:0, pickupType:2 },
|
||||||
|
{ id:'OP005', orderUserId:'u10155', userType:2, orderId:'O005', goodDetailId:'GD105', orderGoodId:'OG205', orderBatchId:'OB305', payStatus:6, sum:4500.00, couponMoney:150.00, couponId:'C202505', freight:60.00, payMoney:4410.00, unitPrice:450.00, totalNum:10, payDate:'2026-07-04 08:30:55', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP006', orderUserId:'u10166', userType:1, orderId:'O006', goodDetailId:'GD106', orderGoodId:'OG206', orderBatchId:'OB306', payStatus:2, sum:13800.00, couponMoney:500.00, couponId:'C202506', freight:200.00, payMoney:13500.00, unitPrice:1380.00, totalNum:10, payDate:'2026-07-05 13:25:30', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP007', orderUserId:'u10177', userType:1, orderId:'O007', goodDetailId:'GD107', orderGoodId:'OG207', orderBatchId:'OB307', payStatus:4, sum:3200.00, couponMoney:0, couponId:'', freight:0, payMoney:3200.00, unitPrice:320.00, totalNum:10, payDate:'2026-07-06 10:12:00', isDel:0, pickupType:2 },
|
||||||
|
{ id:'OP008', orderUserId:'u10188', userType:2, orderId:'O008', goodDetailId:'GD108', orderGoodId:'OG208', orderBatchId:'OB308', payStatus:1, sum:6800.00, couponMoney:0, couponId:'', freight:120.00, payMoney:6920.00, unitPrice:680.00, totalNum:10, payDate:'', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP009', orderUserId:'u10199', userType:1, orderId:'O009', goodDetailId:'GD109', orderGoodId:'OG209', orderBatchId:'OB309', payStatus:3, sum:9800.00, couponMoney:280.00, couponId:'C202509', freight:0, payMoney:9520.00, unitPrice:980.00, totalNum:10, payDate:'2026-07-02 15:48:22', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP010', orderUserId:'u10210', userType:1, orderId:'O010', goodDetailId:'GD110', orderGoodId:'OG210', orderBatchId:'OB310', payStatus:7, sum:5400.00, couponMoney:100.00, couponId:'C202510', freight:60.00, payMoney:5360.00, unitPrice:540.00, totalNum:10, payDate:'2026-06-30 12:30:00', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP011', orderUserId:'u10221', userType:2, orderId:'O011', goodDetailId:'GD111', orderGoodId:'OG211', orderBatchId:'OB311', payStatus:5, sum:18800.00, couponMoney:600.00, couponId:'C202511', freight:300.00, payMoney:18500.00, unitPrice:1880.00, totalNum:10, payDate:'2026-07-03 09:55:08', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP012', orderUserId:'u10232', userType:1, orderId:'O012', goodDetailId:'GD112', orderGoodId:'OG212', orderBatchId:'OB312', payStatus:6, sum:2400.00, couponMoney:0, couponId:'', freight:0, payMoney:2400.00, unitPrice:240.00, totalNum:10, payDate:'2026-07-04 16:20:14', isDel:0, pickupType:2 },
|
||||||
|
{ id:'OP013', orderUserId:'u10243', userType:1, orderId:'O013', goodDetailId:'GD113', orderGoodId:'OG213', orderBatchId:'OB313', payStatus:7, sum:7200.00, couponMoney:240.00, couponId:'C202513', freight:80.00, payMoney:7040.00, unitPrice:720.00, totalNum:10, payDate:'2026-06-25 11:42:35', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP014', orderUserId:'u10254', userType:2, orderId:'O014', goodDetailId:'GD114', orderGoodId:'OG214', orderBatchId:'OB314', payStatus:2, sum:11500.00, couponMoney:400.00, couponId:'C202514', freight:180.00, payMoney:11280.00, unitPrice:1150.00, totalNum:10, payDate:'2026-07-05 14:18:50', isDel:0, pickupType:1 },
|
||||||
|
{ id:'OP015', orderUserId:'u10265', userType:1, orderId:'O015', goodDetailId:'GD115', orderGoodId:'OG215', orderBatchId:'OB315', payStatus:4, sum:4400.00, couponMoney:0, couponId:'', freight:50.00, payMoney:4450.00, unitPrice:440.00, totalNum:10, payDate:'2026-07-06 17:33:21', isDel:0, pickupType:2 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const STATUS_TABS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '1', label: '预付款' },
|
||||||
|
{ value: '2', label: '发货款' },
|
||||||
|
{ value: '3', label: '到货款' },
|
||||||
|
{ value: '4', label: '安装款' },
|
||||||
|
{ value: '5', label: '结算款' },
|
||||||
|
{ value: '6', label: '质保金' },
|
||||||
|
{ value: '7', label: '已结束' }
|
||||||
|
]
|
||||||
|
|
||||||
|
type Col = {
|
||||||
|
prop?: string; label: string; width?: number; slot?: string; fixed?: 'right' | true; type?: 'status' | 'text' | 'selection'
|
||||||
|
}
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 90, fixed: true },
|
||||||
|
{ prop: 'orderId', label: '订单编号', width: 180, slot: 'orderId' },
|
||||||
|
{ prop: 'orderUserId', label: '用户ID', width: 100 },
|
||||||
|
{ type: 'status', prop: 'userType', label: '用户类型', width: 100 },
|
||||||
|
{ prop: 'orderGoodId', label: '小订单ID', width: 120 },
|
||||||
|
{ type: 'status', prop: 'payStatus', label: '款项流程', width: 110 },
|
||||||
|
{ prop: 'sum', label: '优惠前金额', width: 110 },
|
||||||
|
{ prop: 'couponMoney', label: '优惠金额', width: 100 },
|
||||||
|
{ prop: 'freight', label: '运费', width: 80 },
|
||||||
|
{ prop: 'payMoney', label: '实际支付', width: 110 },
|
||||||
|
{ prop: 'unitPrice', label: '单价', width: 90 },
|
||||||
|
{ prop: 'totalNum', label: '待发货总数', width: 100 },
|
||||||
|
{ type: 'status', prop: 'pickupType', label: '配送方式', width: 110 },
|
||||||
|
{ prop: 'payDate', label: '支付时间', width: 160, slot: 'time' },
|
||||||
|
{ prop: 'actions', label: '操作', width: 180, fixed: 'right', slot: 'actions' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const list = ref<PaymentRow[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
const limit = ref(10)
|
||||||
|
const filterKw = ref('')
|
||||||
|
const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api' | 'mock' | 'loading'>('loading')
|
||||||
|
const apiError = ref<string>('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const params: any = { page: page.value, limit: limit.value }
|
||||||
|
if (statusTab.value) params.payStatus = Number(statusTab.value)
|
||||||
|
const data: any = await httpGet<any>(ep, params)
|
||||||
|
const rows: PaymentRow[] = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows
|
||||||
|
total.value = Number(data?.total || rows.length)
|
||||||
|
dataSource.value = 'api'
|
||||||
|
apiError.value = ''
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
// 全部失败 → 前端 seed
|
||||||
|
list.value = [...FALLBACK]
|
||||||
|
total.value = FALLBACK.length
|
||||||
|
dataSource.value = 'mock'
|
||||||
|
apiError.value = '后端 API 暂未实装, 已加载前端示例数据'
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase()
|
||||||
|
let rows = list.value
|
||||||
|
if (statusTab.value) rows = rows.filter(r => String(r.payStatus) === statusTab.value)
|
||||||
|
if (kw) rows = rows.filter(r =>
|
||||||
|
String(r.id).toLowerCase().includes(kw) ||
|
||||||
|
String(r.orderId).toLowerCase().includes(kw) ||
|
||||||
|
String(r.orderGoodId).toLowerCase().includes(kw)
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text'; placeholder?: string }> = [
|
||||||
|
{ key: 'id', label: 'ID', type: 'text', placeholder: '支付ID' },
|
||||||
|
{ key: 'orderId', label: '订单ID', type: 'text', placeholder: '订单ID' },
|
||||||
|
{ key: 'orderGoodId', label: '小订单ID', type: 'text', placeholder: '小订单ID' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() {
|
||||||
|
filterKw.value = String(filters.id || filters.orderId || filters.orderGoodId || '')
|
||||||
|
page.value = 1
|
||||||
|
}
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
async function onMarkPaid(row: PaymentRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认「${row.id}」该款项标记为已支付?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
ElMessage.success('已标记 (前端示例)')
|
||||||
|
}
|
||||||
|
function onViewOrder(row: PaymentRow) {
|
||||||
|
ElMessage.info(`打开订单 ${row.orderId}`)
|
||||||
|
}
|
||||||
|
function onExport() {
|
||||||
|
ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="订单支付回调" sub="实物订单分阶段支付款项管理" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_TABS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:columns="columns"
|
||||||
|
:data="filtered"
|
||||||
|
:loading="loading"
|
||||||
|
:page="page"
|
||||||
|
:limit="limit"
|
||||||
|
:total="total"
|
||||||
|
row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }"
|
||||||
|
@limit-change="(l: number) => { limit = l; page = 1; load() }"
|
||||||
|
>
|
||||||
|
<template #orderId="{ row }">
|
||||||
|
<el-link type="primary" :underline="false" @click="onViewOrder(row)">{{ row.orderId }}</el-link>
|
||||||
|
</template>
|
||||||
|
<template #time="{ row }">{{ row.payDate || '--' }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<el-button type="primary" link @click="onViewOrder(row)">详情</el-button>
|
||||||
|
<el-button v-if="row.payStatus !== 7" type="success" link @click="onMarkPaid(row)">标记已付</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
212
src/modules/order/refund/RefundProcessListView.vue
Normal file
212
src/modules/order/refund/RefundProcessListView.vue
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 订单退款管理 - W15.3
|
||||||
|
*
|
||||||
|
* 沿用老 vue order/orderrefund.vue 字段:
|
||||||
|
* - 选择 + id + userId + supplierId + userEnterpriseId + enterpriseId
|
||||||
|
* - supplierOrderCode + payStatus + payMoney + refundTime + requestTime + remark + delFlag
|
||||||
|
*
|
||||||
|
* 端点: /order/refund/page 探测; 404 → FALLBACK seed (12 条)
|
||||||
|
*
|
||||||
|
* 状态: payStatus 1=待支付 2=已支付 / 退款处理状态: 审核中/已退款/已拒绝
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
|
interface RefundRow {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
supplierId: string
|
||||||
|
userEnterpriseId: string
|
||||||
|
enterpriseId: string
|
||||||
|
supplierOrderCode: string
|
||||||
|
payStatus: number
|
||||||
|
payMoney: number
|
||||||
|
refundTime: string
|
||||||
|
requestTime: string
|
||||||
|
remark: string
|
||||||
|
delFlag: number
|
||||||
|
refundStatus: number // 扩展字段 (老 vue 没有, 用于退款流程: 1=审核中 2=已退款 3=已拒绝 4=已撤销)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAY_LABEL: Record<number, string> = { 1: '待支付', 2: '已支付' }
|
||||||
|
const REFUND_LABEL: Record<number, string> = { 1: '审核中', 2: '已退款', 3: '已拒绝', 4: '已撤销' }
|
||||||
|
const REFUND_TYPE: Record<number, string> = { 1: 'primary', 2: 'success', 3: 'danger', 4: 'info' }
|
||||||
|
|
||||||
|
const ENDPOINTS = ['/order/orderrefund/page', '/order/orderrefund/list']
|
||||||
|
|
||||||
|
const FALLBACK: RefundRow[] = [
|
||||||
|
{ id:'RF001', userId:'u10086', supplierId:'s20015', userEnterpriseId:'ue001', enterpriseId:'e008', supplierOrderCode:'SO20260701001', payStatus:2, payMoney:3280.00, refundTime:'2026-07-02 14:32:11', requestTime:'2026-07-01 11:25:00', remark:'商品质量问题', delFlag:0, refundStatus:2 },
|
||||||
|
{ id:'RF002', userId:'u10112', supplierId:'s20016', userEnterpriseId:'ue002', enterpriseId:'e009', supplierOrderCode:'SO20260701002', payStatus:2, payMoney:5680.00, refundTime:'', requestTime:'2026-07-03 09:42:18', remark:'发货延误', delFlag:0, refundStatus:1 },
|
||||||
|
{ id:'RF003', userId:'u10133', supplierId:'s20017', userEnterpriseId:'ue003', enterpriseId:'e010', supplierOrderCode:'SO20260702001', payStatus:2, payMoney:1880.00, refundTime:'2026-06-29 16:00:00', requestTime:'2026-06-28 13:18:30', remark:'规格不符', delFlag:0, refundStatus:3 },
|
||||||
|
{ id:'RF004', userId:'u10144', supplierId:'s20018', userEnterpriseId:'ue004', enterpriseId:'e011', supplierOrderCode:'SO20260702002', payStatus:1, payMoney:0, refundTime:'', requestTime:'', remark:'', delFlag:0, refundStatus:4 },
|
||||||
|
{ id:'RF005', userId:'u10155', supplierId:'s20019', userEnterpriseId:'ue005', enterpriseId:'e012', supplierOrderCode:'SO20260703001', payStatus:2, payMoney:4410.00, refundTime:'2026-07-04 11:08:55', requestTime:'2026-07-03 18:32:00', remark:'协商一致退款', delFlag:0, refundStatus:2 },
|
||||||
|
{ id:'RF006', userId:'u10166', supplierId:'s20020', userEnterpriseId:'ue006', enterpriseId:'e013', supplierOrderCode:'SO20260703002', payStatus:2, payMoney:13500.00, refundTime:'', requestTime:'2026-07-05 10:14:42', remark:'部分退款申请', delFlag:0, refundStatus:1 },
|
||||||
|
{ id:'RF007', userId:'u10177', supplierId:'s20021', userEnterpriseId:'ue007', enterpriseId:'e014', supplierOrderCode:'SO20260704001', payStatus:2, payMoney:3200.00, refundTime:'2026-07-06 14:30:00', requestTime:'2026-07-05 16:48:22', remark:'客户主动撤销', delFlag:0, refundStatus:4 },
|
||||||
|
{ id:'RF008', userId:'u10188', supplierId:'s20022', userEnterpriseId:'ue008', enterpriseId:'e015', supplierOrderCode:'SO20260704002', payStatus:2, payMoney:6920.00, refundTime:'', requestTime:'2026-07-06 09:55:08', remark:'物流丢件', delFlag:0, refundStatus:1 },
|
||||||
|
{ id:'RF009', userId:'u10199', supplierId:'s20023', userEnterpriseId:'ue009', enterpriseId:'e016', supplierOrderCode:'SO20260705001', payStatus:2, payMoney:9520.00, refundTime:'2026-07-02 17:20:14', requestTime:'2026-07-01 11:42:35', remark:'商品损坏', delFlag:0, refundStatus:2 },
|
||||||
|
{ id:'RF010', userId:'u10210', supplierId:'s20024', userEnterpriseId:'ue010', enterpriseId:'e017', supplierOrderCode:'SO20260705002', payStatus:2, payMoney:5360.00, refundTime:'2026-06-30 13:30:00', requestTime:'2026-06-28 16:18:50', remark:'客户拒收', delFlag:0, refundStatus:3 },
|
||||||
|
{ id:'RF011', userId:'u10221', supplierId:'s20025', userEnterpriseId:'ue011', enterpriseId:'e018', supplierOrderCode:'SO20260706001', payStatus:2, payMoney:18500.00, refundTime:'2026-07-05 14:18:50', requestTime:'2026-07-04 09:25:00', remark:'换货协商一致折现', delFlag:0, refundStatus:2 },
|
||||||
|
{ id:'RF012', userId:'u10232', supplierId:'s20026', userEnterpriseId:'ue012', enterpriseId:'e019', supplierOrderCode:'SO20260706002', payStatus:2, payMoney:2400.00, refundTime:'', requestTime:'2026-07-06 11:02:18', remark:'批次问题', delFlag:0, refundStatus:1 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const STATUS_TABS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '1', label: '审核中' },
|
||||||
|
{ value: '2', label: '已退款' },
|
||||||
|
{ value: '3', label: '已拒绝' },
|
||||||
|
{ value: '4', label: '已撤销' }
|
||||||
|
]
|
||||||
|
|
||||||
|
type Col = {
|
||||||
|
prop?: string; label: string; width?: number; slot?: string; fixed?: 'right' | true; type?: 'status' | 'text' | 'selection'
|
||||||
|
}
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ prop: 'id', label: '退款ID', width: 100, fixed: true },
|
||||||
|
{ prop: 'supplierOrderCode', label: '供应商订单号', width: 200, slot: 'orderCode' },
|
||||||
|
{ prop: 'userId', label: '用户ID', width: 90 },
|
||||||
|
{ prop: 'supplierId', label: '商户ID', width: 90 },
|
||||||
|
{ prop: 'enterpriseId', label: '商户企业ID', width: 110 },
|
||||||
|
{ prop: 'userEnterpriseId', label: '用户企业ID', width: 110 },
|
||||||
|
{ type: 'status', prop: 'payStatus', label: '原支付状态', width: 100 },
|
||||||
|
{ prop: 'payMoney', label: '实际支付金额', width: 120 },
|
||||||
|
{ type: 'status', prop: 'refundStatus', label: '退款状态', width: 100 },
|
||||||
|
{ prop: 'remark', label: '退款原因', width: 200, slot: 'remark' },
|
||||||
|
{ prop: 'requestTime', label: '申请时间', width: 160, slot: 'requestTime' },
|
||||||
|
{ prop: 'refundTime', label: '退款完成时间', width: 160, slot: 'refundTime' },
|
||||||
|
{ prop: 'actions', label: '操作', width: 200, fixed: 'right', slot: 'actions' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const list = ref<RefundRow[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
const limit = ref(10)
|
||||||
|
const filterKw = ref('')
|
||||||
|
const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api' | 'mock' | 'loading'>('loading')
|
||||||
|
const apiError = ref<string>('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const params: any = { page: page.value, limit: limit.value }
|
||||||
|
const data: any = await httpGet<any>(ep, params)
|
||||||
|
const rows: RefundRow[] = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows
|
||||||
|
total.value = Number(data?.total || rows.length)
|
||||||
|
dataSource.value = 'api'
|
||||||
|
apiError.value = ''
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]
|
||||||
|
total.value = FALLBACK.length
|
||||||
|
dataSource.value = 'mock'
|
||||||
|
apiError.value = '后端 API 暂未实装, 已加载前端示例数据'
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase()
|
||||||
|
let rows = list.value
|
||||||
|
if (statusTab.value) rows = rows.filter(r => String(r.refundStatus) === statusTab.value)
|
||||||
|
if (kw) rows = rows.filter(r =>
|
||||||
|
String(r.id).toLowerCase().includes(kw) ||
|
||||||
|
String(r.supplierOrderCode).toLowerCase().includes(kw) ||
|
||||||
|
String(r.userId).toLowerCase().includes(kw)
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [
|
||||||
|
{ key: 'id', label: '退款ID', type: 'text' },
|
||||||
|
{ key: 'supplierOrderCode', label: '供应商订单号', type: 'text' },
|
||||||
|
{ key: 'userId', label: '用户ID', type: 'text' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() {
|
||||||
|
filterKw.value = String(filters.id || filters.supplierOrderCode || filters.userId || '')
|
||||||
|
page.value = 1
|
||||||
|
}
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
|
||||||
|
async function onApprove(row: RefundRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认通过退款申请「${row.id}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
row.refundStatus = 2
|
||||||
|
row.refundTime = new Date().toLocaleString('zh-CN')
|
||||||
|
ElMessage.success('已通过 (前端示例)')
|
||||||
|
}
|
||||||
|
async function onReject(row: RefundRow) {
|
||||||
|
try {
|
||||||
|
const { value } = await ElMessageBox.prompt('请填写拒绝原因', '拒绝退款', { inputPlaceholder: '原因', confirmButtonText: '拒绝', cancelButtonText: '取消' })
|
||||||
|
row.refundStatus = 3
|
||||||
|
row.remark = (value || '已拒绝').trim()
|
||||||
|
ElMessage.success('已拒绝 (前端示例)')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
function onView(row: RefundRow) {
|
||||||
|
ElMessage.info(`打开退款详情 ${row.id}`)
|
||||||
|
}
|
||||||
|
function onExport() {
|
||||||
|
ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="退款管理" sub="实物订单退款流程审核 + 跟踪" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_TABS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:columns="columns"
|
||||||
|
:data="filtered"
|
||||||
|
:loading="loading"
|
||||||
|
:page="page"
|
||||||
|
:limit="limit"
|
||||||
|
:total="total"
|
||||||
|
row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }"
|
||||||
|
@limit-change="(l: number) => { limit = l; page = 1; load() }"
|
||||||
|
>
|
||||||
|
<template #orderCode="{ row }">
|
||||||
|
<el-link type="primary" :underline="false" @click="onView(row)">{{ row.supplierOrderCode }}</el-link>
|
||||||
|
</template>
|
||||||
|
<template #remark="{ row }">
|
||||||
|
<span :class="{ 'rr-muted': !row.remark }">{{ row.remark || '--' }}</span>
|
||||||
|
</template>
|
||||||
|
<template #requestTime="{ row }">{{ row.requestTime || '--' }}</template>
|
||||||
|
<template #refundTime="{ row }">{{ row.refundTime || '--' }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<template v-if="row.refundStatus === 1">
|
||||||
|
<el-button type="success" link @click="onApprove(row)">通过</el-button>
|
||||||
|
<el-button type="danger" link @click="onReject(row)">拒绝</el-button>
|
||||||
|
</template>
|
||||||
|
<el-button v-else type="primary" link @click="onView(row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
.rr-muted { color: var(--ink-muted); }
|
||||||
|
</style>
|
||||||
55
src/modules/other/category/CategoryDeepListView.vue
Normal file
55
src/modules/other/category/CategoryDeepListView.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 行业/资讯分类 - P0 真 view (共用组件, props.type 区分)
|
||||||
|
* 走 /other/category/categoryDeepPageList (type: 1=行业 3=资讯分类)
|
||||||
|
* 2026-08-07 探测: type=1 total=27, type=3 total=4
|
||||||
|
* 字段: id/name/level/pid/sort/status/createDate/children
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface Category { id: string; name: string; level: number; pid: string; sort: number; status: number; createDate: string; children?: Category[] }
|
||||||
|
|
||||||
|
const props = defineProps<{ type?: number; title?: string }>()
|
||||||
|
const TITLES: Record<number, string> = { 1: '行业管理', 3: '资讯分类' }
|
||||||
|
|
||||||
|
const list = ref<Category[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 100 })
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
|
{ prop: 'name', label: '名称', minWidth: 200 },
|
||||||
|
{ prop: 'level', label: '层级', width: 80, formatter: (r: Category) => ({ 1: '一级', 2: '二级', 3: '三级', 4: '四级' } as Record<number, string>)[r.level] || String(r.level) },
|
||||||
|
{ prop: 'sort', label: '排序', width: 80 },
|
||||||
|
{ prop: 'status', label: '状态', width: 90, slot: 'status' },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 180 }
|
||||||
|
]
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r: any = await httpGet('/other/category/categoryDeepPageList', { type: props.type ?? 1, page: query.page, limit: query.limit })
|
||||||
|
list.value = r?.list || []
|
||||||
|
total.value = Number(r?.total || 0)
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader :title="props.title || TITLES[props.type ?? 1] || '分类管理'" :sub="`共 ${total} 条`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<DataTable :columns="columns" :data="list" :borderless="true" :loading="loading" :total="total" :page="query.page" :limit="query.limit" :show-pagination="false" row-key="id" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" empty-text="暂无数据" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 商品分类 - W6 P0 完整化
|
* F006 商品分类 - W10 A2 标准化
|
||||||
|
*
|
||||||
|
* W10 A2: 套 PageHeader/SearchBar (保留 el-table 树形, DataTable 不支持树形)
|
||||||
*
|
*
|
||||||
* 沿用老 vue other/categoryGood.vue 273 行:
|
* 沿用老 vue other/categoryGood.vue 273 行:
|
||||||
* - 搜索: 分类名
|
* - 搜索: 分类名
|
||||||
@ -13,6 +15,9 @@
|
|||||||
import { ref, onMounted, reactive, computed } from 'vue'
|
import { ref, onMounted, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
id: string
|
id: string
|
||||||
@ -82,14 +87,28 @@ async function load() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await new Promise(r => setTimeout(r, 200))
|
const resp: any = await httpGet('/other/category/categoryDeepPageList', { type: 2, page: 1, limit: 200 })
|
||||||
tree.value = TREE_DATA
|
const rawList = resp?.list || []
|
||||||
|
tree.value = rawList.map((r: any) => normalize(r))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.msg || e?.message || '加载失败'
|
error.value = e?.msg || e?.message || '加载失败'
|
||||||
tree.value = TREE_DATA
|
tree.value = TREE_DATA
|
||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalize(raw: any): Category {
|
||||||
|
return {
|
||||||
|
id: String(raw.id || ''),
|
||||||
|
pid: String(raw.pid || '0'),
|
||||||
|
name: raw.name || '',
|
||||||
|
level: raw.level || 1,
|
||||||
|
sort: raw.sort || 0,
|
||||||
|
createDate: raw.createDate || '',
|
||||||
|
raw,
|
||||||
|
children: (raw.children || []).map((c: any) => normalize(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onSearch() {}
|
function onSearch() {}
|
||||||
function onReset() { filters.name = '' }
|
function onReset() { filters.name = '' }
|
||||||
function onAdd(parent?: Category) {
|
function onAdd(parent?: Category) {
|
||||||
@ -138,28 +157,19 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="商品分类" :sub="`商品 3 级分类管理 (含子级: ${tree.length} 个一级分类)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">商品分类</h2>
|
|
||||||
<p class="rr-page-sub">商品 3 级分类管理 (含子级: {{ tree.length }} 个一级分类)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" @click="onAdd()">新增一级分类</el-button>
|
<el-button type="primary" @click="onAdd()">新增一级分类</el-button>
|
||||||
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
<SearchBar
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
v-model:filters="filters"
|
||||||
<el-form-item label="分类名称">
|
:fields="[
|
||||||
<el-input v-model="filters.name" placeholder="请输入分类名" clearable style="width: 240px" />
|
{ key: 'name', label: '分类名称', type: 'text', placeholder: '请输入分类名' }
|
||||||
</el-form-item>
|
]"
|
||||||
<el-form-item>
|
@search="onSearch"
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
@reset="onReset"
|
||||||
<el-button @click="onReset">重置</el-button>
|
/>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<el-table
|
<el-table
|
||||||
@ -171,6 +181,7 @@ onMounted(load)
|
|||||||
borderless
|
borderless
|
||||||
empty-text="暂无分类"
|
empty-text="暂无分类"
|
||||||
class="cat-table"
|
class="cat-table"
|
||||||
|
:header-cell-style="{ textAlign: 'center' }"
|
||||||
@selection-change="onSelectionChange"
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="50" />
|
<el-table-column type="selection" width="50" />
|
||||||
@ -189,11 +200,16 @@ onMounted(load)
|
|||||||
<el-button text type="primary" size="small" @click="onMove(row, 2)">下移</el-button>
|
<el-button text type="primary" size="small" @click="onMove(row, 2)">下移</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="320" align="center" fixed="right">
|
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="row.level < 3" text type="primary" size="small" @click="onAdd(row)">新增子分类</el-button>
|
<TableActions
|
||||||
<el-button text type="warning" size="small" @click="onEdit(row)">修改</el-button>
|
:row="row"
|
||||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
:actions="[
|
||||||
|
{ label: '修改', type: 'primary', onClick: onEdit },
|
||||||
|
{ label: '新增子分类', type: 'primary', when: (r: any) => r.level < 3, onClick: onAdd },
|
||||||
|
{ label: '删除', type: 'danger', onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -225,10 +241,5 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1500px; }
|
.rr-page { max-width: 1500px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.cat-table { background: transparent; }
|
.cat-table { background: transparent; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 商品单位 - W3.x C P1.1
|
* F006 商品单位 - W10 A4 标准化
|
||||||
|
*
|
||||||
|
* W10 A4: 套 PageHeader/SearchBar 标准化模板 (前端筛选转 SearchBar)
|
||||||
* 走 /other/goodunit/list 真 API (返 data: [...] 数组, 共 112 行)
|
* 走 /other/goodunit/list 真 API (返 data: [...] 数组, 共 112 行)
|
||||||
* 字段: id/name/type/supplierId/enterpriseId/createTime
|
* 字段: id/name/type/supplierId/enterpriseId/createTime
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { onMounted, ref, computed, reactive } 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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface GoodUnit {
|
interface GoodUnit {
|
||||||
id: string
|
id: string
|
||||||
@ -22,6 +26,7 @@ const list = ref<GoodUnit[]>([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const filterKw = ref('')
|
const filterKw = ref('')
|
||||||
|
const filters = reactive({ kw: filterKw })
|
||||||
|
|
||||||
const TYPE_MAP: Record<number, string> = {
|
const TYPE_MAP: Record<number, string> = {
|
||||||
1: '系统',
|
1: '系统',
|
||||||
@ -51,7 +56,7 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filtered = computed(() => {
|
const filtered = computed(() => {
|
||||||
const kw = filterKw.value.trim().toLowerCase()
|
const kw = (filters.kw || "").trim().toLowerCase()
|
||||||
if (!kw) return list.value
|
if (!kw) return list.value
|
||||||
return list.value.filter(it => it.name.toLowerCase().includes(kw))
|
return list.value.filter(it => it.name.toLowerCase().includes(kw))
|
||||||
})
|
})
|
||||||
@ -66,14 +71,17 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="商品单位" :sub="`商品计量单位管理 (共 ${list.length} 个, 筛选后 ${filtered.length} 个)`" />
|
||||||
<h2 class="rr-page-title">商品单位</h2>
|
|
||||||
<p class="rr-page-sub">商品计量单位管理(共 {{ list.length }} 个, 筛选后 {{ filtered.length }} 个)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters">
|
<SearchBar
|
||||||
<el-input v-model="filterKw" placeholder="按名称筛选" clearable class="rr-filter-input" size="default" />
|
v-model:filters="filters"
|
||||||
</div>
|
:fields="[
|
||||||
|
{ key: 'kw', label: '名称', type: 'text', placeholder: '按名称筛选' }
|
||||||
|
]"
|
||||||
|
:show-reset="false"
|
||||||
|
@search="() => {}"
|
||||||
|
@reset="() => { filterKw = '' }"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -90,7 +98,7 @@ onMounted(load)
|
|||||||
:total="filtered.length"
|
:total="filtered.length"
|
||||||
:page="1"
|
:page="1"
|
||||||
:limit="filtered.length || 1"
|
:limit="filtered.length || 1"
|
||||||
:show-pagination="list.length > 20"
|
:show-pagination="list.length > 20" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无单位"
|
empty-text="暂无单位"
|
||||||
borderless
|
borderless
|
||||||
@ -112,11 +120,5 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
|
||||||
.rr-filters { margin-bottom: 16px; display: flex; gap: 8px; }
|
|
||||||
.rr-filter-input { width: 280px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
|
||||||
.rr-time { font-family: 'SF Mono', Menlo, monospace; font-size: 12px; color: var(--ink-2); }
|
.rr-time { font-family: 'SF Mono', Menlo, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 拼团活动 - W6 P0 完整化
|
* F006 拼团活动 - W10 A3 标准化
|
||||||
|
*
|
||||||
|
* W10 A3: 套 PageHeader/SearchBar 标准化模板
|
||||||
*
|
*
|
||||||
* 沿用老 vue other/groupbuy.vue 116 行:
|
* 沿用老 vue other/groupbuy.vue 116 行:
|
||||||
* - 搜索: 活动名称
|
* - 搜索: 活动名称
|
||||||
@ -12,6 +14,8 @@
|
|||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Groupbuy {
|
interface Groupbuy {
|
||||||
id: string
|
id: string
|
||||||
@ -98,25 +102,18 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="拼团活动" :sub="`拼团/团购活动管理 (共 ${total} 个)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">拼团活动</h2>
|
|
||||||
<p class="rr-page-sub">拼团/团购活动管理 (共 {{ total }} 个)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
|
||||||
<el-form-item label="活动名称">
|
|
||||||
<el-input v-model="filters.name" placeholder="请输入活动名称" clearable style="width: 280px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="onReset">重置</el-button>
|
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
|
||||||
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
||||||
</el-form-item>
|
</PageHeader>
|
||||||
</el-form>
|
|
||||||
</div>
|
<SearchBar
|
||||||
|
v-model:filters="filters"
|
||||||
|
:fields="[
|
||||||
|
{ key: 'name', label: '活动名称', type: 'text', placeholder: '请输入活动名称' }
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<div class="rr-pagination">
|
<div class="rr-pagination">
|
||||||
@ -145,10 +142,5 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
62
src/modules/other/help/HelpListView.vue
Normal file
62
src/modules/other/help/HelpListView.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 帮助中心 - P1 真 view
|
||||||
|
*
|
||||||
|
* 走 /other/help/page
|
||||||
|
* 真实接口 (2026-08-07 探测: 200, total=1)
|
||||||
|
* 字段: id/title/content/pid/sort/createTime
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface HelpItem {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
pid: string
|
||||||
|
sort: number
|
||||||
|
createTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = ref<HelpItem[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
|
{ prop: 'title', label: '标题', minWidth: 240 },
|
||||||
|
{ prop: 'sort', label: '排序', width: 90, formatter: (r: HelpItem) => String(r.sort ?? 0) },
|
||||||
|
{ prop: 'createTime', label: '创建时间', width: 180 }
|
||||||
|
]
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const resp: any = await httpGet('/other/help/page', { page: query.page, limit: query.limit })
|
||||||
|
list.value = resp?.list || []
|
||||||
|
total.value = Number(resp?.total || 0)
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="帮助中心" :sub="`帮助文档 (共 ${total} 条)`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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"
|
||||||
|
@limit-change="onLimitChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
61
src/modules/other/information/InformationListView.vue
Normal file
61
src/modules/other/information/InformationListView.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 资讯列表 - P1 真 view
|
||||||
|
*
|
||||||
|
* 走 /other/information/page
|
||||||
|
* 真实接口 (2026-08-07 探测: 200, total=7)
|
||||||
|
* 字段: id/title/categoryId/categoryName/content/createTime
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface Info {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
categoryId: string
|
||||||
|
categoryName: string
|
||||||
|
createTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = ref<Info[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 90 },
|
||||||
|
{ prop: 'title', label: '资讯标题', minWidth: 280 },
|
||||||
|
{ prop: 'categoryName', label: '分类', width: 140, formatter: (r: Info) => r.categoryName || '--' },
|
||||||
|
{ prop: 'createTime', label: '发布时间', width: 180 }
|
||||||
|
]
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const resp: any = await httpGet('/other/information/page', { page: query.page, limit: query.limit })
|
||||||
|
list.value = resp?.list || []
|
||||||
|
total.value = Number(resp?.total || 0)
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="资讯列表" :sub="`平台资讯 (共 ${total} 条)`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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"
|
||||||
|
@limit-change="onLimitChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 标签管理 - 完整版 (W6 整改)
|
* F006 标签管理 - W10 A1 标准化
|
||||||
|
*
|
||||||
|
* W10 A1: 套 PageHeader/SearchBar 标准化模板
|
||||||
*
|
*
|
||||||
* 沿用老 vue other/label.vue 173 行:
|
* 沿用老 vue other/label.vue 173 行:
|
||||||
* - 搜索: 标签名
|
* - 搜索: 标签名
|
||||||
@ -15,6 +17,9 @@ import { ref, onMounted, reactive } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface Label {
|
interface Label {
|
||||||
id: string
|
id: string
|
||||||
@ -103,27 +108,18 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="标签管理" :sub="`商品/活动/服务标签分类管理 (共 ${total} 个)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">标签管理</h2>
|
|
||||||
<p class="rr-page-sub">商品/活动/服务标签分类管理 (共 {{ total }} 个)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" @click="onAdd">新增标签</el-button>
|
<el-button type="primary" @click="onAdd">新增标签</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
<SearchBar
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
v-model:filters="filters"
|
||||||
<el-form-item label="标签名称">
|
:fields="[
|
||||||
<el-input v-model="filters.name" placeholder="请输入标签名" clearable style="width: 240px" />
|
{ key: 'name', label: '标签名称', type: 'text', placeholder: '请输入标签名' }
|
||||||
</el-form-item>
|
]"
|
||||||
<el-form-item>
|
@search="onSearch"
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
@reset="onReset"
|
||||||
<el-button @click="onReset">重置</el-button>
|
/>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -131,14 +127,14 @@ onMounted(load)
|
|||||||
{ prop: 'name', label: '标签名称', minWidth: 200 },
|
{ prop: 'name', label: '标签名称', minWidth: 200 },
|
||||||
{ prop: 'createDate', label: '创建时间', width: 180 },
|
{ prop: 'createDate', label: '创建时间', width: 180 },
|
||||||
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
||||||
{ label: '操作', width: 240, align: 'center', slot: 'action' }
|
{ label: '操作', width: 140, align: 'center', slot: 'action' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="total > 10"
|
:show-pagination="total > 10" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无标签"
|
empty-text="暂无标签"
|
||||||
borderless
|
borderless
|
||||||
@ -152,11 +148,14 @@ onMounted(load)
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<el-button text type="primary" size="small" @click="onEdit(row)">修改</el-button>
|
<TableActions
|
||||||
<el-button text :type="row.status === 0 ? 'warning' : 'success'" size="small" @click="onToggleStatus(row)">
|
:row="row"
|
||||||
{{ row.status === 0 ? '禁用' : '启用' }}
|
:actions="[
|
||||||
</el-button>
|
{ label: '修改', type: 'primary', onClick: onEdit },
|
||||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
{ label: (r: any) => r.status === 0 ? '禁用' : '启用', type: (r: any) => r.status === 0 ? 'warning' : 'success', onClick: onToggleStatus },
|
||||||
|
{ label: '删除', type: 'danger', onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
@ -184,9 +183,4 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 操作指南 - W3.x C P0.4
|
* F006 操作指南 - W10 A7 标准化
|
||||||
|
*
|
||||||
|
* W10 A7: 套 PageHeader (无搜索, 简洁)
|
||||||
* 走 /other/operateguide/page
|
* 走 /other/operateguide/page
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive } 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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
interface Guide {
|
interface Guide {
|
||||||
id: string
|
id: string
|
||||||
@ -20,7 +23,7 @@ interface Guide {
|
|||||||
const list = ref<Guide[]>([])
|
const list = ref<Guide[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const query = reactive({ page: 1, limit: 20 })
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ prop: 'id', label: 'ID', width: 80 },
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
@ -38,25 +41,22 @@ async function load() {
|
|||||||
} 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() }
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="操作指南" :sub="`操作指南内容 (共 ${total} 条)`" />
|
||||||
<h2 class="rr-page-title">内容列表</h2>
|
|
||||||
<p class="rr-page-sub">操作指南内容(共 {{ total }} 条)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<DataTable :columns="columns" :data="list" :loading="loading" :total="total" :page="query.page" :limit="query.limit" :show-pagination="true" row-key="id" empty-text="暂无内容" @page-change="onPageChange" />
|
<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"
|
||||||
|
@limit-change="onLimitChange" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.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-sub { color: var(--ink-3); font-size: 13px; }
|
|
||||||
.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>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,256 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 价格公式系统变量管理 (运营后台)
|
||||||
|
*
|
||||||
|
* 走 /supplier/supplierGoodPriceFormulaVariable/ API
|
||||||
|
* 功能: 变量 CRUD + 快速改值 (改后 SKU 公式价格自动变化)
|
||||||
|
* 字段: varKey/varName/varUnit/currentValue/sourceType/sourceCode/formula/sort/status
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface Variable {
|
||||||
|
id: string
|
||||||
|
varKey: string
|
||||||
|
varName: string
|
||||||
|
varUnit: string
|
||||||
|
currentValue: number
|
||||||
|
sourceType: number
|
||||||
|
sourceCode: string
|
||||||
|
formula: string
|
||||||
|
sort: number
|
||||||
|
status: number
|
||||||
|
createDate: string
|
||||||
|
updateDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = ref<Variable[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const limit = ref(20)
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
// 新增/编辑弹窗
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const form = reactive<any>({
|
||||||
|
id: null, varKey: '', varName: '', varUnit: '', currentValue: 0,
|
||||||
|
sourceType: 1, sourceCode: '', formula: '', sort: 0, status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 快速改值
|
||||||
|
const quickEditId = ref<string | null>(null)
|
||||||
|
const quickValue = ref('')
|
||||||
|
|
||||||
|
const SOURCE_MAP: Record<number, string> = { 1: '手工维护', 2: 'AKShare采集' }
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet('/supplier/supplierGoodPriceFormulaVariable/page', {
|
||||||
|
page: page.value, limit: limit.value, keyword: keyword.value || undefined
|
||||||
|
})
|
||||||
|
list.value = (data?.list || []).map((it: any) => ({
|
||||||
|
id: String(it.id), varKey: it.var_key || '--', varName: it.var_name || '--',
|
||||||
|
varUnit: it.var_unit || '', currentValue: Number(it.current_value || 0),
|
||||||
|
sourceType: Number(it.source_type || 1), sourceCode: it.source_code || '',
|
||||||
|
formula: it.formula || '', sort: Number(it.sort || 0), status: Number(it.status || 1),
|
||||||
|
createDate: it.create_date || '--', updateDate: it.update_date || '--'
|
||||||
|
}))
|
||||||
|
total.value = Number(data?.total || 0)
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '加载失败')
|
||||||
|
list.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() { page.value = 1; load() }
|
||||||
|
function onReset() { keyword.value = ''; page.value = 1; load() }
|
||||||
|
function onPageChange(p: number) { page.value = p; load() }
|
||||||
|
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||||
|
|
||||||
|
function openAdd() {
|
||||||
|
Object.assign(form, { id: null, varKey: '', varName: '', varUnit: '', currentValue: 0, sourceType: 1, sourceCode: '', formula: '', sort: 0, status: 1 })
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: Variable) {
|
||||||
|
Object.assign(form, {
|
||||||
|
id: row.id, varKey: row.varKey, varName: row.varName, varUnit: row.varUnit,
|
||||||
|
currentValue: row.currentValue, sourceType: row.sourceType, sourceCode: row.sourceCode,
|
||||||
|
formula: row.formula, sort: row.sort, status: row.status
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!form.varKey.trim() || !form.varName.trim()) {
|
||||||
|
ElMessage.warning('变量标识和变量名必填'); return
|
||||||
|
}
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await httpPost('/supplier/supplierGoodPriceFormulaVariable/save', { ...form })
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '保存失败')
|
||||||
|
} finally { saving.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickUpdateValue(row: Variable) {
|
||||||
|
if (quickValue.value === '') return
|
||||||
|
try {
|
||||||
|
await httpPost('/supplier/supplierGoodPriceFormulaVariable/updateValue', { id: row.id, currentValue: quickValue.value })
|
||||||
|
ElMessage.success(`「${row.varName}」值已更新为 ${quickValue.value}, 相关商品价格将自动变化`)
|
||||||
|
quickEditId.value = null
|
||||||
|
quickValue.value = ''
|
||||||
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(row: Variable) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除变量「${row.varName}」? 引用该变量的商品公式将无法计算`, '删除确认', { type: 'warning' })
|
||||||
|
await httpGet('/supplier/supplierGoodPriceFormulaVariable/delete', { id: row.id })
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e === 'cancel' || e === 'close') return
|
||||||
|
ElMessage.error(e?.msg || e?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStatus(row: Variable) {
|
||||||
|
const next = row.status === 1 ? 0 : 1
|
||||||
|
httpPost('/supplier/supplierGoodPriceFormulaVariable/save', {
|
||||||
|
id: row.id, varKey: row.varKey, varName: row.varName, varUnit: row.varUnit,
|
||||||
|
currentValue: row.currentValue, sourceType: row.sourceType, sourceCode: row.sourceCode,
|
||||||
|
formula: row.formula, sort: row.sort, status: next
|
||||||
|
}).then(() => {
|
||||||
|
ElMessage.success(next === 1 ? '已启用' : '已禁用')
|
||||||
|
load()
|
||||||
|
}).catch((e: any) => ElMessage.error(e?.msg || e?.message || '操作失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="价格变量管理" :sub="`系统级价格变量 (共 ${total} 个), 修改变量值后所有引用商品的售价自动更新`">
|
||||||
|
<el-button type="primary" @click="openAdd">新增变量</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="rr-card">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<el-input v-model="keyword" placeholder="按变量名/标识搜索" clearable style="width: 260px" @keyup.enter="onSearch" />
|
||||||
|
<el-button type="primary" @click="onSearch">搜索</el-button>
|
||||||
|
<el-button @click="onReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:columns="[
|
||||||
|
{ prop: 'varName', label: '变量名', width: 120 },
|
||||||
|
{ prop: 'varKey', label: '标识', width: 140 },
|
||||||
|
{ prop: 'varUnit', label: '单位', width: 80 },
|
||||||
|
{ prop: 'currentValue', label: '当前值', width: 130, slot: 'value' },
|
||||||
|
{ prop: 'sourceType', label: '取值方式', width: 110, slot: 'source' },
|
||||||
|
{ prop: 'sourceCode', label: '采集代码', width: 100 },
|
||||||
|
{ prop: 'status', label: '状态', width: 80, slot: 'status' },
|
||||||
|
{ prop: 'updateDate', label: '更新时间', width: 170 },
|
||||||
|
{ prop: 'action', label: '操作', width: 220, slot: 'action', fixed: 'right' }
|
||||||
|
]"
|
||||||
|
:data="list"
|
||||||
|
:loading="loading"
|
||||||
|
:total="total"
|
||||||
|
:page="page"
|
||||||
|
:limit="limit"
|
||||||
|
:show-pagination="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
@limit-change="onLimitChange"
|
||||||
|
empty-text="暂无变量"
|
||||||
|
borderless
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #value="{ row }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-bold text-primary">¥{{ Number(row.currentValue).toLocaleString() }}</span>
|
||||||
|
<template v-if="quickEditId === row.id">
|
||||||
|
<el-input v-model="quickValue" size="small" style="width: 90px" placeholder="新值" @keyup.enter="quickUpdateValue(row)" />
|
||||||
|
<el-button size="small" type="primary" @click="quickUpdateValue(row)">确认</el-button>
|
||||||
|
<el-button size="small" @click="quickEditId = null; quickValue = ''">取消</el-button>
|
||||||
|
</template>
|
||||||
|
<el-button v-else size="small" text type="primary" @click="quickEditId = row.id; quickValue = String(row.currentValue)">改值</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #source="{ row }">
|
||||||
|
<el-tag :type="row.sourceType === 2 ? 'warning' : 'info'" size="small" effect="light">
|
||||||
|
{{ SOURCE_MAP[row.sourceType] || '未知' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small" effect="light">
|
||||||
|
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-link type="primary" :underline="false" @click="openEdit(row)">编辑</el-link>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-link type="warning" :underline="false" @click="toggleStatus(row)">{{ row.status === 1 ? '禁用' : '启用' }}</el-link>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-link type="danger" :underline="false" @click="remove(row)">删除</el-link>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑变量' : '新增变量'" width="520px" destroy-on-close>
|
||||||
|
<el-form label-width="90px" label-position="left">
|
||||||
|
<el-form-item label="变量标识" required>
|
||||||
|
<el-input v-model="form.varKey" placeholder="如 copper_price (英文/下划线, 唯一)" :disabled="!!form.id" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="变量名" required>
|
||||||
|
<el-input v-model="form.varName" placeholder="如 铜价 (公式中显示的名称)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="单位">
|
||||||
|
<el-input v-model="form.varUnit" placeholder="如 元/吨" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="当前值">
|
||||||
|
<el-input-number v-model="form.currentValue" :precision="4" :step="100" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="取值方式">
|
||||||
|
<el-radio-group v-model="form.sourceType">
|
||||||
|
<el-radio :value="1">手工维护</el-radio>
|
||||||
|
<el-radio :value="2">AKShare采集</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.sourceType === 2" label="采集代码">
|
||||||
|
<el-input v-model="form.sourceCode" placeholder="AKShare 商品代码, 如 copper/aluminum/steel" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.sourceType === 2" label="换算公式">
|
||||||
|
<el-input v-model="form.formula" placeholder="可选, 如 raw * 1.05 (raw=采集原始值)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number v-model="form.sort" :min="0" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 秒杀活动 - W6 P0 完整化
|
* F006 秒杀活动 - W10 A3 标准化
|
||||||
|
*
|
||||||
|
* W10 A3: 套 PageHeader/SearchBar 标准化模板
|
||||||
*
|
*
|
||||||
* 沿用老 vue other/seckill.vue 113 行:
|
* 沿用老 vue other/seckill.vue 113 行:
|
||||||
* - 搜索: 活动名称
|
* - 搜索: 活动名称
|
||||||
@ -12,6 +14,8 @@
|
|||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Seckill {
|
interface Seckill {
|
||||||
id: string
|
id: string
|
||||||
@ -99,25 +103,18 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="秒杀活动" :sub="`限时秒杀活动管理 (共 ${total} 个)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">秒杀活动</h2>
|
|
||||||
<p class="rr-page-sub">限时秒杀活动管理 (共 {{ total }} 个)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
|
||||||
<el-form-item label="活动名称">
|
|
||||||
<el-input v-model="filters.name" placeholder="请输入活动名称" clearable style="width: 280px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="onReset">重置</el-button>
|
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
|
||||||
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
||||||
</el-form-item>
|
</PageHeader>
|
||||||
</el-form>
|
|
||||||
</div>
|
<SearchBar
|
||||||
|
v-model:filters="filters"
|
||||||
|
:fields="[
|
||||||
|
{ key: 'name', label: '活动名称', type: 'text', placeholder: '请输入活动名称' }
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<div class="rr-pagination">
|
<div class="rr-pagination">
|
||||||
@ -146,10 +143,5 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,21 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 供应商品牌审核 - W6 P0 完整化
|
* F006 品牌管理 (供应商品牌审核) - 真页面重写 2026-07-28
|
||||||
*
|
*
|
||||||
* 沿用老 vue other/supplierBrand.vue 230 行:
|
* 对齐老 vue other/supplierBrand.vue:
|
||||||
* - 搜索: 品牌名/企业名/审核状态/申请时间范围
|
* - 搜索: 品牌名称/企业名称/审核状态/申请时间范围
|
||||||
* - 表格列: sourceEnterpriseName/enterpriseName/brandName/authUrl(50px缩略)/status(1待/2通过/3拒绝)/createDate
|
* - 表格列: 品牌来源/企业名称/品牌名称/授权书(50px缩略)/审核状态/申请时间
|
||||||
* - 操作: 审核(仅待审) / 删除
|
* - 操作: 审核(仅待审) / 删除 / 批量删除
|
||||||
* - 审核弹窗: 通过/拒绝 + 拒绝原因
|
* - 审核弹窗: 通过/拒绝 + 拒绝原因 (POST /other/GoodBrand/updateCheck)
|
||||||
* - 批量删除
|
* 需求3: 查看授权企业 — 平台品牌(sourceType=1)可查看被授权企业列表 (sourceType=2 同品牌)
|
||||||
* - /other/supplierBrand 后端 404 → FALLBACK mock
|
*
|
||||||
|
* 端点:
|
||||||
|
* GET /other/GoodBrand/page?page=&limit=&brandName=&enterpriseName=&status=&startDate=&endDate=&sourceType=
|
||||||
|
* POST /other/GoodBrand/updateCheck {id,status,failName}
|
||||||
|
* POST /other/GoodBrand/delete [ids]
|
||||||
*/
|
*/
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Brand {
|
interface Brand {
|
||||||
id: string
|
id: string
|
||||||
|
sourceType: number // 1平台新增 2授权
|
||||||
sourceEnterpriseName: string
|
sourceEnterpriseName: string
|
||||||
enterpriseName: string
|
enterpriseName: string
|
||||||
brandName: string
|
brandName: string
|
||||||
@ -32,49 +39,68 @@ const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
|||||||
3: { label: '审核拒绝', type: 'danger' }
|
3: { label: '审核拒绝', type: 'danger' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK: Brand[] = [
|
|
||||||
{ id: 'B001', sourceEnterpriseName: '中国钢铁工业协会', enterpriseName: '宝钢集团', brandName: '宝钢', authUrl: 'https://placehold.co/100x100/2563EB/fff?text=Auth1', status: 1, createDate: '2026-07-01 10:00:00', raw: {} },
|
|
||||||
{ id: 'B002', sourceEnterpriseName: '中国水泥协会', enterpriseName: '海螺水泥', brandName: '海螺', authUrl: 'https://placehold.co/100x100/059669/fff?text=Auth2', status: 2, createDate: '2026-06-28 14:20:00', raw: {} },
|
|
||||||
{ id: 'B003', sourceEnterpriseName: '国家建材测试中心', enterpriseName: '北新建材', brandName: '龙牌', authUrl: 'https://placehold.co/100x100/DC2626/fff?text=Auth3', status: 3, createDate: '2026-06-25 11:15:00', failName: '授权书扫描件不清晰, 请重新上传', raw: {} },
|
|
||||||
{ id: 'B004', sourceEnterpriseName: '中国建材联合会', enterpriseName: '金隅集团', brandName: '金隅', authUrl: 'https://placehold.co/100x100/5645D4/fff?text=Auth4', status: 1, createDate: '2026-07-02 16:30:00', raw: {} },
|
|
||||||
{ id: 'B005', sourceEnterpriseName: '中国五金制品协会', enterpriseName: '长城精工', brandName: '长城', authUrl: 'https://placehold.co/100x100/F59E0B/fff?text=Auth5', status: 2, createDate: '2026-06-30 09:00:00', raw: {} },
|
|
||||||
{ id: 'B006', sourceEnterpriseName: '中国建筑材料联合会', enterpriseName: '华润水泥', brandName: '华润', authUrl: 'https://placehold.co/100x100/8B5CF6/fff?text=Auth6', status: 1, createDate: '2026-07-03 11:45:00', raw: {} }
|
|
||||||
]
|
|
||||||
|
|
||||||
const list = ref<Brand[]>([])
|
const list = ref<Brand[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const limit = ref(10)
|
const limit = ref(10)
|
||||||
const filters = reactive({ brandName: '', enterpriseName: '', status: '', startDate: '', endDate: '' })
|
const filters = reactive({ brandName: '', enterpriseName: '', status: '', range: [] as string[], _startDate: '', _endDate: '' })
|
||||||
const selection = ref<Brand[]>([])
|
const selection = ref<Brand[]>([])
|
||||||
|
|
||||||
const auditVisible = ref(false)
|
const auditVisible = ref(false)
|
||||||
const auditForm = reactive<{ id: string; status: number; failName: string }>({ id: '', status: 2, failName: '' })
|
const auditForm = reactive<{ id: string; status: number; failName: string }>({ id: '', status: 2, failName: '' })
|
||||||
|
|
||||||
|
// 授权企业弹窗
|
||||||
|
const authVisible = ref(false)
|
||||||
|
const authBrandName = ref('')
|
||||||
|
const authList = ref<{ enterpriseName: string; sourceEnterpriseName: string; status: number; createDate: string }[]>([])
|
||||||
|
const authLoading = ref(false)
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await new Promise(r => setTimeout(r, 200))
|
const params: any = { page: page.value, limit: limit.value }
|
||||||
let rows = [...FALLBACK]
|
if (filters.brandName) params.brandName = filters.brandName
|
||||||
if (filters.brandName) rows = rows.filter(r => r.brandName.includes(filters.brandName))
|
if (filters.enterpriseName) params.enterpriseName = filters.enterpriseName
|
||||||
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
|
if (filters.status !== '') params.status = filters.status
|
||||||
if (filters.status) rows = rows.filter(r => r.status === Number(filters.status))
|
if (filters.range?.length === 2) {
|
||||||
list.value = rows
|
params.startDate = `${filters.range[0]} 00:00:00`
|
||||||
total.value = rows.length
|
params.endDate = `${filters.range[1]} 23:59:59`
|
||||||
|
}
|
||||||
|
const data: any = await httpGet('/other/GoodBrand/page', params)
|
||||||
|
const rows: any[] = data?.list || []
|
||||||
|
list.value = rows.map((it: any) => ({
|
||||||
|
id: String(it.id),
|
||||||
|
sourceType: it.sourceType ?? 1,
|
||||||
|
sourceEnterpriseName: it.sourceEnterpriseName || '--',
|
||||||
|
enterpriseName: it.enterpriseName || '--',
|
||||||
|
brandName: it.brandName || '--',
|
||||||
|
authUrl: it.authUrl || '',
|
||||||
|
status: Number(it.status) || 1,
|
||||||
|
createDate: it.createDate || '--',
|
||||||
|
failName: it.failName,
|
||||||
|
raw: it
|
||||||
|
}))
|
||||||
|
total.value = Number(data?.total) || 0
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.msg || e?.message || '加载失败'
|
error.value = e?.msg || e?.message || '加载失败'
|
||||||
list.value = FALLBACK
|
list.value = []
|
||||||
total.value = FALLBACK.length
|
total.value = 0
|
||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSearch() { page.value = 1; load() }
|
function onSearch() { page.value = 1; load() }
|
||||||
function onReset() { filters.brandName = ''; filters.enterpriseName = ''; filters.status = ''; filters.startDate = ''; filters.endDate = ''; load() }
|
function onReset() {
|
||||||
|
filters.brandName = ''
|
||||||
|
filters.enterpriseName = ''
|
||||||
|
filters.status = ''
|
||||||
|
filters.range = []
|
||||||
|
onSearch()
|
||||||
|
}
|
||||||
function onPageChange(p: number) { page.value = p; load() }
|
function onPageChange(p: number) { page.value = p; load() }
|
||||||
function onLimitChange(l: number) { limit.value = l; load() }
|
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||||
function onSelectionChange(rows: Brand[]) { selection.value = rows }
|
function onSelectionChange(rows: Brand[]) { selection.value = rows }
|
||||||
|
|
||||||
function onAudit(row: Brand) {
|
function onAudit(row: Brand) {
|
||||||
@ -83,24 +109,63 @@ function onAudit(row: Brand) {
|
|||||||
auditForm.failName = ''
|
auditForm.failName = ''
|
||||||
auditVisible.value = true
|
auditVisible.value = true
|
||||||
}
|
}
|
||||||
function onSubmitAudit() {
|
async function onSubmitAudit() {
|
||||||
if (auditForm.status === 3 && !auditForm.failName.trim()) { ElMessage.warning('请填写拒绝原因'); return }
|
if (auditForm.status === 3 && !auditForm.failName.trim()) { ElMessage.warning('请填写拒绝原因'); return }
|
||||||
ElMessage.success(`审核${auditForm.status === 2 ? '通过' : '拒绝'}成功 (mock)`)
|
try {
|
||||||
|
await httpPost('/other/GoodBrand/updateCheck', {
|
||||||
|
id: auditForm.id,
|
||||||
|
status: auditForm.status,
|
||||||
|
failName: auditForm.status === 3 ? auditForm.failName.trim() : ''
|
||||||
|
})
|
||||||
|
ElMessage.success(`审核${auditForm.status === 2 ? '通过' : '拒绝'}成功`)
|
||||||
auditVisible.value = false
|
auditVisible.value = false
|
||||||
load()
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '审核失败')
|
||||||
}
|
}
|
||||||
function onDelete(row: Brand) {
|
}
|
||||||
ElMessageBox.confirm(`确认删除「${row.brandName}」?`, '提示', { type: 'warning' }).then(() => {
|
|
||||||
ElMessage.success('已删除 (mock)')
|
async function onViewAuth(row: Brand) {
|
||||||
|
authBrandName.value = row.brandName
|
||||||
|
authVisible.value = true
|
||||||
|
authList.value = []
|
||||||
|
authLoading.value = true
|
||||||
|
try {
|
||||||
|
// 该品牌名下所有授权记录 (sourceType=2)
|
||||||
|
const data: any = await httpGet('/other/GoodBrand/page', { page: 1, limit: 100, brandName: row.brandName, sourceType: 2 })
|
||||||
|
authList.value = (data?.list || []).map((it: any) => ({
|
||||||
|
enterpriseName: it.enterpriseName || '--',
|
||||||
|
sourceEnterpriseName: it.sourceEnterpriseName || '--',
|
||||||
|
status: Number(it.status) || 1,
|
||||||
|
createDate: it.createDate || '--'
|
||||||
|
}))
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '加载授权企业失败')
|
||||||
|
authList.value = []
|
||||||
|
} finally { authLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDelete(row: Brand) {
|
||||||
|
await ElMessageBox.confirm(`确认删除「${row.brandName}」?`, '提示', { type: 'warning' })
|
||||||
|
try {
|
||||||
|
await httpPost('/other/GoodBrand/delete', [row.id])
|
||||||
|
ElMessage.success('删除成功')
|
||||||
load()
|
load()
|
||||||
}).catch(() => {})
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '删除失败')
|
||||||
}
|
}
|
||||||
function onBatchDelete() {
|
}
|
||||||
|
|
||||||
|
async function onBatchDelete() {
|
||||||
if (selection.value.length === 0) { ElMessage.warning('请先选择要删除的项'); return }
|
if (selection.value.length === 0) { ElMessage.warning('请先选择要删除的项'); return }
|
||||||
ElMessageBox.confirm(`确认批量删除选中的 ${selection.value.length} 项?`, '提示', { type: 'warning' }).then(() => {
|
await ElMessageBox.confirm(`确认批量删除选中的 ${selection.value.length} 项?`, '提示', { type: 'warning' })
|
||||||
ElMessage.success('已批量删除 (mock)')
|
try {
|
||||||
|
await httpPost('/other/GoodBrand/delete', selection.value.map(s => s.id))
|
||||||
|
ElMessage.success('删除成功')
|
||||||
load()
|
load()
|
||||||
}).catch(() => {})
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '删除失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
@ -108,39 +173,71 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="品牌管理" :sub="`供应商品牌审核与授权管理 (共 ${total} 个)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">供应商品牌审核</h2>
|
|
||||||
<p class="rr-page-sub">供应商提交的品牌授权审核 (共 {{ total }} 个)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
|
||||||
<el-form-item label="品牌名称">
|
|
||||||
<el-input v-model="filters.brandName" placeholder="请输入品牌名" clearable style="width: 180px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="企业名称">
|
|
||||||
<el-input v-model="filters.enterpriseName" placeholder="请输入企业名" clearable style="width: 180px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="审核状态">
|
|
||||||
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 140px">
|
|
||||||
<el-option label="待审核" :value="1" />
|
|
||||||
<el-option label="审核通过" :value="2" />
|
|
||||||
<el-option label="审核拒绝" :value="3" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
|
||||||
<el-button @click="onReset">重置</el-button>
|
|
||||||
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
||||||
</el-form-item>
|
</PageHeader>
|
||||||
</el-form>
|
|
||||||
</div>
|
<SearchBar
|
||||||
|
v-model:filters="filters"
|
||||||
|
:fields="[
|
||||||
|
{ key: 'brandName', label: '品牌名称', type: 'text', placeholder: '请输入品牌名' },
|
||||||
|
{ key: 'enterpriseName', label: '企业名称', type: 'text', placeholder: '请输入企业名' },
|
||||||
|
{ key: 'status', label: '审核状态', type: 'select', options: [
|
||||||
|
{ value: 1, label: '待审核' },
|
||||||
|
{ value: 2, label: '审核通过' },
|
||||||
|
{ value: 3, label: '审核拒绝' }
|
||||||
|
] },
|
||||||
|
{ key: 'range', label: '申请时间', type: 'daterange' }
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
|
<el-table v-loading="loading" :data="list" border @selection-change="onSelectionChange" style="width: 100%">
|
||||||
|
<el-table-column type="selection" header-align="center" align="center" width="50" />
|
||||||
|
<el-table-column prop="sourceType" label="类型" header-align="center" align="center" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.sourceType === 2 ? 'primary' : 'info'" size="small">{{ row.sourceType === 2 ? '授权品牌' : '平台品牌' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="sourceEnterpriseName" label="品牌来源" header-align="center" align="center" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="enterpriseName" label="企业名称" header-align="center" align="center" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="brandName" label="品牌名称" header-align="center" align="center" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="品牌授权书" header-align="center" align="center" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image v-if="row.authUrl" :src="row.authUrl" fit="cover" :preview-src-list="[row.authUrl]" class="brand-auth" />
|
||||||
|
<span v-else class="rr-muted">--</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="审核状态" header-align="center" align="center" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="STATUS_MAP[row.status]?.type || 'info'" size="small">{{ STATUS_MAP[row.status]?.label || row.status }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createDate" label="申请时间" header-align="center" align="center" width="170" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="rr-actions">
|
||||||
|
<el-link v-if="row.sourceType === 1" type="primary" :underline="false" @click="onViewAuth(row)">查看授权</el-link>
|
||||||
|
<span v-if="row.sourceType === 1" class="rr-actions__sep">|</span>
|
||||||
|
<el-link v-if="row.status === 1" type="warning" :underline="false" @click="onAudit(row)">审核</el-link>
|
||||||
|
<span v-if="row.status === 1" class="rr-actions__sep">|</span>
|
||||||
|
<el-link type="danger" :underline="false" @click="onDelete(row)">删除</el-link>
|
||||||
|
</span>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -161,16 +258,31 @@ onMounted(load)
|
|||||||
<el-button type="primary" @click="onSubmitAudit">确定</el-button>
|
<el-button type="primary" @click="onSubmitAudit">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="authVisible" :title="`「${authBrandName}」授权企业`" width="640px">
|
||||||
|
<el-table v-loading="authLoading" :data="authList" border max-height="400">
|
||||||
|
<el-table-column prop="enterpriseName" 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">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="STATUS_MAP[row.status]?.type || 'info'" size="small">{{ STATUS_MAP[row.status]?.label || row.status }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createDate" label="申请时间" header-align="center" align="center" width="170" />
|
||||||
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="authVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1500px; }
|
.rr-page { max-width: 1500px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
.rr-card { background: var(--el-bg-color); border-radius: 8px; border: 1px solid var(--el-border-color-light); overflow: hidden; }
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--el-border-color-light); }
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
.brand-auth { width: 50px; height: 50px; border-radius: 4px; border: 1px solid var(--el-border-color-light); cursor: pointer; }
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
.rr-muted { color: var(--el-text-color-placeholder); }
|
||||||
.rr-search { padding: 16px 20px; }
|
.rr-actions { display: inline-flex; align-items: center; gap: 0; white-space: nowrap; }
|
||||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
.rr-actions__sep { color: var(--el-border-color); margin: 0 6px; }
|
||||||
.brand-auth { width: 50px; height: 50px; border-radius: 4px; border: 1px solid var(--border); cursor: pointer; }
|
|
||||||
</style>
|
</style>
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 常量配置 - 完整版 (W6 整改)
|
* F006 常量配置 - W10 A1 标准化
|
||||||
|
*
|
||||||
|
* W10 A1: 套 PageHeader/SearchBar 标准化模板
|
||||||
*
|
*
|
||||||
* 沿用老 vue other/textconfig.vue 85 行:
|
* 沿用老 vue other/textconfig.vue 85 行:
|
||||||
* - 搜索: 描述
|
* - 搜索: 描述
|
||||||
@ -14,6 +16,8 @@
|
|||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface TextConfig {
|
interface TextConfig {
|
||||||
id: string
|
id: string
|
||||||
@ -97,27 +101,18 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="常量配置" :sub="`系统常量/协议/横幅配置 (共 ${total} 条)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">常量配置</h2>
|
|
||||||
<p class="rr-page-sub">系统常量/协议/横幅配置 (共 {{ total }} 条)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" @click="onAdd">新增常量</el-button>
|
<el-button type="primary" @click="onAdd">新增常量</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
<SearchBar
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
v-model:filters="filters"
|
||||||
<el-form-item label="描述">
|
:fields="[
|
||||||
<el-input v-model="filters.descr" placeholder="请输入描述" clearable style="width: 240px" />
|
{ key: 'descr', label: '描述', type: 'text', placeholder: '请输入描述' }
|
||||||
</el-form-item>
|
]"
|
||||||
<el-form-item>
|
@search="onSearch"
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
@reset="onReset"
|
||||||
<el-button @click="onReset">重置</el-button>
|
/>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -134,7 +129,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="total > 10"
|
:show-pagination="total > 10" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无常量"
|
empty-text="暂无常量"
|
||||||
borderless
|
borderless
|
||||||
@ -196,11 +191,6 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.rr-content-text { color: var(--ink); font-size: 13px; }
|
.rr-content-text { color: var(--ink); font-size: 13px; }
|
||||||
.rr-richtext { line-height: 1.6; color: var(--ink); }
|
.rr-richtext { line-height: 1.6; color: var(--ink); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -83,7 +83,8 @@ async function checkApi() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res = await fetch(apiUrl.value, {
|
const res = await fetch(apiUrl.value, {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
credentials: 'omit'
|
||||||
})
|
})
|
||||||
// W4.19 修复:nginx 没代理的业务模块会返 404 HTML 页, 不是 JSON
|
// W4.19 修复:nginx 没代理的业务模块会返 404 HTML 页, 不是 JSON
|
||||||
// 检测 content-type 防止误判
|
// 检测 content-type 防止误判
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
/**
|
|
||||||
* F006 商品店铺 - W3.x C P2
|
|
||||||
* 后端 /product/shop/page 端点状态: 404 / 400 (端点不存在或参数错)
|
|
||||||
* 走 el-result 优雅占位 + 展示参考老 /vue 后台的预期字段
|
|
||||||
*/
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { httpGet } from '@/utils/request'
|
|
||||||
|
|
||||||
interface FieldSpec { key: string; label: string; desc: string }
|
|
||||||
|
|
||||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
|
||||||
const errorMsg = ref('')
|
|
||||||
|
|
||||||
const expectedFields: FieldSpec[] = [
|
|
||||||
{ key: 'icon', label: '封面', desc: '图片缩略图' },
|
|
||||||
{ key: 'title', label: '商品名称', desc: '商品标题' },
|
|
||||||
{ key: 'enterpriseName', label: '供应商', desc: '所属企业名' },
|
|
||||||
{ key: 'firstCategoryName', label: '一级分类', desc: '一级分类名' },
|
|
||||||
{ key: 'secondCategoryName', label: '二级分类', desc: '二级分类名' },
|
|
||||||
{ key: 'thirdCategoryName', label: '三级分类', desc: '三级分类名' },
|
|
||||||
{ key: 'brandName', label: '品牌', desc: '品牌名' },
|
|
||||||
{ key: 'unitName', label: '单位', desc: '计量单位' },
|
|
||||||
{ key: 'createDate', label: '申请时间', desc: '提交时间' },
|
|
||||||
{ key: 'onShelf', label: '铺货', desc: '0=未铺货/1=已铺货' },
|
|
||||||
{ key: 'checkOpinion', label: '审核意见', desc: '审核备注' },
|
|
||||||
{ key: 'checkStatus', label: '状态', desc: '1=待审核/2=出售中/3=已驳回' }
|
|
||||||
]
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const r: any = await httpGet('/product/shop/page', { page: 1, limit: 1 })
|
|
||||||
if (r?.code === 0) status.value = '200'
|
|
||||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
|
||||||
status.value = '404'
|
|
||||||
errorMsg.value = '后端 API 未实现 (/product/shop/page 返回 404)'
|
|
||||||
} else {
|
|
||||||
status.value = 'error'
|
|
||||||
errorMsg.value = e?.msg || e?.message || '请求失败'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="rr-page">
|
|
||||||
<div class="rr-page-header">
|
|
||||||
<h2 class="rr-page-title">商品店铺</h2>
|
|
||||||
<p class="rr-page-sub">商品管理 (supplier/good 同款) - 数据接口未实现, 占位页展示预期字段</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-card rr-status-card">
|
|
||||||
<el-result
|
|
||||||
:icon="status === '200' ? 'success' : 'warning'"
|
|
||||||
:title="status === '200' ? 'API 已就绪' : '后端 API 未实现'"
|
|
||||||
:sub-title="status === '200' ? '商品店铺 数据接口已通, 可正常加载' : (errorMsg || '请联系后端实现 /product/shop/page 接口')"
|
|
||||||
>
|
|
||||||
<template #extra>
|
|
||||||
<el-tag v-if="status === 'checking'" type="info">检测中...</el-tag>
|
|
||||||
<el-tag v-else-if="status === '404'" type="warning">404 Not Found</el-tag>
|
|
||||||
<el-tag v-else-if="status === '200'" type="success">200 OK</el-tag>
|
|
||||||
<el-tag v-else type="danger">Error</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-result>
|
|
||||||
</div>
|
|
||||||
<div class="rr-card rr-fields-card">
|
|
||||||
<h3 class="rr-section-title">预期字段 (参考老 /vue 后台)</h3>
|
|
||||||
<el-table :data="expectedFields" border size="small" :show-header="true" empty-text="无字段">
|
|
||||||
<el-table-column prop="key" label="字段名 (key)" width="220" />
|
|
||||||
<el-table-column prop="label" label="表头 (label)" width="180" />
|
|
||||||
<el-table-column prop="desc" label="说明" />
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.rr-page { max-width: 1400px; }
|
|
||||||
.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-sub { color: var(--ink-3); font-size: 13px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 24px; margin-bottom: 16px; }
|
|
||||||
.rr-status-card { padding: 20px 20px 8px; }
|
|
||||||
.rr-section-title { margin: 0 0 12px; font-size: 14px; font-weight: 600; color: var(--ink); }
|
|
||||||
</style>
|
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 营销活动管理
|
* F006 营销活动管理 - W10 A7 标准化
|
||||||
|
*
|
||||||
|
* W10 A7: 套 PageHeader/SearchBar 标准化模板
|
||||||
*
|
*
|
||||||
* 后端端点 (ActivityZoneController @ /supplier/activityzone):
|
* 后端端点 (ActivityZoneController @ /supplier/activityzone):
|
||||||
* GET /supplier/activityzone/page 分页列表
|
* GET /supplier/activityzone/page 分页列表
|
||||||
@ -9,6 +11,8 @@
|
|||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive } 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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Activity {
|
interface Activity {
|
||||||
id: string
|
id: string
|
||||||
@ -98,24 +102,20 @@ onMounted(() => { load() })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-user">
|
<div class="rr-sys-user">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="营销活动" :sub="`共 ${total} 个活动`" />
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">营销活动</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个活动</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.activityName" placeholder="活动名搜索" clearable class="rr-username-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-select v-model="filters.status" placeholder="状态" class="rr-type-select" clearable size="small">
|
:fields="[
|
||||||
<el-option value="1" label="开启" />
|
{ key: 'activityName', label: '活动名', type: 'text', placeholder: '活动名搜索' },
|
||||||
<el-option value="0" label="关闭" />
|
{ key: 'status', label: '状态', type: 'select', options: [
|
||||||
</el-select>
|
{ value: 1, label: '开启' },
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
{ value: 0, label: '关闭' }
|
||||||
</div>
|
] }
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -128,12 +128,12 @@ onMounted(() => { load() })
|
|||||||
{ prop: 'endTime', label: '结束时间', width: 170, slot: 'endTime' },
|
{ prop: 'endTime', label: '结束时间', width: 170, slot: 'endTime' },
|
||||||
{ label: '操作', width: 100, slot: 'action' }
|
{ label: '操作', width: 100, slot: 'action' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list" :borderless="true"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="loadError"
|
:error="loadError"
|
||||||
empty-text="暂无活动"
|
empty-text="暂无活动"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@ -184,15 +184,7 @@ onMounted(() => { load() })
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-user { padding: 16px; }
|
.rr-sys-user { padding: 16px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.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; }
|
|
||||||
.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; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 优惠券管理 - W7 P0 整改 (沿用老 vue supplier/coupon.vue 116 行)
|
* F006 优惠券管理 - W10 A4 标准化
|
||||||
|
*
|
||||||
|
* W10 A4: 套 PageHeader/SearchBar 标准化模板 (沿用老 vue supplier/coupon.vue 116 行)
|
||||||
*
|
*
|
||||||
* 端点 /supplier/coupon/page 200 OK 真数据, 老字段:
|
* 端点 /supplier/coupon/page 200 OK 真数据, 老字段:
|
||||||
* - 搜索: 优惠券名
|
* - 搜索: 优惠券名
|
||||||
@ -11,6 +13,9 @@
|
|||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface Coupon {
|
interface Coupon {
|
||||||
id: string
|
id: string
|
||||||
@ -119,31 +124,22 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="优惠券管理" :sub="`平台/企业优惠券管理 (共 ${total} 张, 选中 ${selection.length})`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">优惠券管理</h2>
|
|
||||||
<p class="rr-page-sub">平台/企业优惠券管理 (共 {{ total }} 张, 选中 {{ selection.length }})</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" @click="onAdd">新增优惠券</el-button>
|
<el-button type="primary" @click="onAdd">新增优惠券</el-button>
|
||||||
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除</el-button>
|
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
<SearchBar
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
v-model:filters="filters"
|
||||||
<el-form-item label="优惠券名称">
|
:fields="[
|
||||||
<el-input v-model="filters.name" placeholder="请输入优惠券名称" clearable style="width: 280px" />
|
{ key: 'name', label: '优惠券名称', type: 'text', placeholder: '请输入优惠券名称' }
|
||||||
</el-form-item>
|
]"
|
||||||
<el-form-item>
|
@search="onSearch"
|
||||||
<el-button @click="onReset">重置</el-button>
|
@reset="onReset"
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
/>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<el-table :data="list" border style="width: 100%" v-loading="loading" :empty-text="error || '暂无优惠券'" @selection-change="onSelectionChange" row-key="id">
|
<el-table :data="list" border style="width: 100%" v-loading="loading" :empty-text="error || '暂无优惠券'" @selection-change="onSelectionChange" row-key="id" :header-cell-style="{ textAlign: 'center' }">
|
||||||
<el-table-column type="selection" width="50" align="center" />
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
<el-table-column type="index" label="#" width="50" align="center" />
|
<el-table-column type="index" label="#" width="50" align="center" />
|
||||||
<el-table-column prop="name" label="优惠券名称" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="name" label="优惠券名称" min-width="180" show-overflow-tooltip />
|
||||||
@ -184,13 +180,17 @@ onMounted(load)
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="row.sourceType === 2" text type="primary" size="small" @click="onEdit(row)">编辑</el-button>
|
<TableActions
|
||||||
<el-button v-if="row.onShelf === 1" text type="warning" size="small" @click="onToggleStatus(row)">停用</el-button>
|
:row="row"
|
||||||
<el-button v-else text type="success" size="small" @click="onToggleStatus(row)">启用</el-button>
|
:actions="[
|
||||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
{ label: '编辑', type: 'primary', when: (r: any) => r.sourceType === 2, onClick: onEdit },
|
||||||
<el-button v-if="row.sourceType === 2" text type="info" size="small" @click="onShowRecords(row)">领取记录</el-button>
|
{ label: (r: any) => r.onShelf === 1 ? '停用' : '启用', type: (r: any) => r.onShelf === 1 ? 'warning' : 'success', onClick: onToggleStatus },
|
||||||
|
{ label: '领取记录', type: 'primary', when: (r: any) => r.sourceType === 2, onClick: onShowRecords },
|
||||||
|
{ label: '删除', type: 'danger', divided: true, onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -219,15 +219,5 @@ onMounted(load)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1500px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
|
||||||
.rr-muted { color: var(--ink-3); }
|
|
||||||
.rr-stock-ok { color: var(--primary); font-weight: 600; }
|
|
||||||
.rr-stock-out { color: var(--ink-3); }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
118
src/modules/supplier/couponscope/CouponScopeListView.vue
Normal file
118
src/modules/supplier/couponscope/CouponScopeListView.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 优惠券适用范围 - W16
|
||||||
|
*
|
||||||
|
* 沿用老 vue supplier/couponscope.vue 字段
|
||||||
|
*
|
||||||
|
* 端点: 4 URL 探测 (couponscope/page, /list, /search, /queryPage); 404 → FALLBACK seed
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
|
interface CouponScopeRow { id:string; couponId:string; supplierGoodId:string; sourceType:number; createTime:string }
|
||||||
|
const ENDPOINTS = [
|
||||||
|
'/supplier/couponscope/page',
|
||||||
|
'/supplier/couponscope/list',
|
||||||
|
'/supplier/couponscope/search',
|
||||||
|
'/supplier/couponscope/queryPage'
|
||||||
|
]
|
||||||
|
const FALLBACK: CouponScopeRow[] = [
|
||||||
|
{ id:'CS001', couponId:'C202501', supplierGoodId:'SG101', sourceType:1, createTime:'2026-04-15 09:30:00' },
|
||||||
|
{ id:'CS002', couponId:'C202502', supplierGoodId:'SG102', sourceType:1, createTime:'2026-04-22 14:22:00' },
|
||||||
|
{ id:'CS003', couponId:'C202503', supplierGoodId:'', sourceType:2, createTime:'2026-05-08 11:08:30' },
|
||||||
|
{ id:'CS004', couponId:'C202504', supplierGoodId:'SG104', sourceType:1, createTime:'2026-05-15 16:48:00' },
|
||||||
|
{ id:'CS005', couponId:'C202505', supplierGoodId:'', sourceType:2, createTime:'2026-05-22 09:15:00' },
|
||||||
|
{ id:'CS006', couponId:'C202506', supplierGoodId:'SG106', sourceType:1, createTime:'2026-06-03 10:42:00' },
|
||||||
|
{ id:'CS007', couponId:'C202507', supplierGoodId:'SG107', sourceType:1, createTime:'2026-06-12 13:18:00' },
|
||||||
|
{ id:'CS008', couponId:'C202508', supplierGoodId:'', sourceType:2, createTime:'2026-06-20 11:35:00' },
|
||||||
|
{ id:'CS009', couponId:'C202509', supplierGoodId:'SG109', sourceType:1, createTime:'2026-06-28 14:55:00' },
|
||||||
|
{ id:'CS010', couponId:'C202510', supplierGoodId:'SG110', sourceType:1, createTime:'2026-07-04 09:50:00' },
|
||||||
|
{ id:'CS011', couponId:'C202511', supplierGoodId:'', sourceType:2, createTime:'2026-07-08 16:08:00' },
|
||||||
|
{ id:'CS012', couponId:'C202512', supplierGoodId:'SG112', sourceType:1, createTime:'2026-07-12 11:42:00' }
|
||||||
|
]
|
||||||
|
type Col = { type?: 'status'|'text'|'selection'; prop?: string; label: string; width?: number; slot?: string; fixed?: 'right'|true }
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ type:'selection', label:'', width:50 },
|
||||||
|
{ prop:'id', label:'范围ID', width:100, fixed:true },
|
||||||
|
{ prop:'couponId', label:'优惠券ID', width:130, slot:'couponId' },
|
||||||
|
{ prop:'supplierGoodId', label:'适用商品ID', width:140, slot:'goodId' },
|
||||||
|
{ prop:'sourceType', label:'卷来源', width:100, slot:'src' },
|
||||||
|
{ prop:'createTime', label:'创建时间', width:160, slot:'time' },
|
||||||
|
{ prop:'actions', label:'操作', width:200, slot:'actions', fixed:'right' }
|
||||||
|
]
|
||||||
|
const STATUS_DEFAULTS = [
|
||||||
|
{ value:'', label:'全部' },
|
||||||
|
{ value:'1', label:'供应商来源' },
|
||||||
|
{ value:'2', label:'平台来源' }
|
||||||
|
]
|
||||||
|
const list = ref<CouponScopeRow[]>([]); const total = ref(0); const loading = ref(false); const page = ref(1); const limit = ref(10); const filterKw = ref(''); const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api'|'mock'|'loading'>('loading'); const apiError = ref<string>('')
|
||||||
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet<any>(ep, { page: page.value, limit: limit.value })
|
||||||
|
const rows = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows; total.value = Number(data?.total || rows.length); dataSource.value = 'api'; apiError.value = ''; loading.value = false; return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]; total.value = FALLBACK.length; dataSource.value = 'mock'; apiError.value = '后端 API 暂未实装, 已加载前端示例数据'; loading.value = false
|
||||||
|
}
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase(); let rs = list.value
|
||||||
|
if (statusTab.value) rs = rs.filter(r => String(r.sourceType) === statusTab.value)
|
||||||
|
if (kw) rs = rs.filter(r => String(r.id).includes(kw) || String(r.couponId).includes(kw) || String(r.supplierGoodId).includes(kw))
|
||||||
|
return rs
|
||||||
|
})
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [{ key:'id', label:'范围ID', type:'text' },{ key:'couponId', label:'优惠券ID', type:'text' },{ key:'supplierGoodId', label:'商品ID', type:'text' }]
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() { filterKw.value = String(filters.id || filters.couponId || filters.supplierGoodId || ''); page.value = 1 }
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
async function onDelete(row: CouponScopeRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认删除适用范围「${row.id}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
ElMessage.success('已删除 (前端示例)')
|
||||||
|
}
|
||||||
|
function onView(row: CouponScopeRow) { ElMessage.info(`详情 ${row.id}`) }
|
||||||
|
function onExport() { ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)') }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="优惠券适用范围" sub="优惠券可使用商品范围配置" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_DEFAULTS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
<DataTable :columns="columns" :data="filtered"
|
||||||
|
:borderless="true" :loading="loading" :page="page" :limit="limit" :total="total" row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }" @limit-change="(l: number) => { limit = l; page = 1; load() }">
|
||||||
|
<template #couponId="{ row }">
|
||||||
|
<el-link v-if="row.couponId" type="primary" :underline="false" @click="onView(row)">{{ row.couponId }}</el-link>
|
||||||
|
<span v-else class="rr-muted">通用券</span>
|
||||||
|
</template>
|
||||||
|
<template #goodId="{ row }">
|
||||||
|
<span v-if="row.supplierGoodId">{{ row.supplierGoodId }}</span>
|
||||||
|
<span v-else class="rr-muted">全场通用</span>
|
||||||
|
</template>
|
||||||
|
<template #src="{ row }">
|
||||||
|
<el-tag :type="row.sourceType === 1 ? 'primary' : 'success'" size="small">{{ row.sourceType === 1 ? '供应商' : '平台' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #time="{ row }">{{ row.createTime }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<el-button type="primary" link @click="onView(row)">详情</el-button>
|
||||||
|
<el-button type="danger" link @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
@ -6,7 +6,7 @@
|
|||||||
* - 搜索: 商品名/供应商/分类/品牌
|
* - 搜索: 商品名/供应商/分类/品牌
|
||||||
* - 状态 tab: 全部/待审核/已通过/已驳回/已下架
|
* - 状态 tab: 全部/待审核/已通过/已驳回/已下架
|
||||||
* - 表格列: 封面/商品/供应商/3级分类/品牌/单位/申请时间/铺货/审核意见/状态
|
* - 表格列: 封面/商品/供应商/3级分类/品牌/单位/申请时间/铺货/审核意见/状态
|
||||||
* - 操作: 详情/审核/下架/修改分类/删除
|
* - 操作: 详情(常驻) + 审核/上下架/改分类/删除(收纳⋯) [W17.6 主次分离]
|
||||||
* - 批量: 批量审核/批量驳回/批量下架/批量改分类/批量删除
|
* - 批量: 批量审核/批量驳回/批量下架/批量改分类/批量删除
|
||||||
* - 详情弹窗: 含 SKU 列表 (编码/组合/封面/原价/现价/秒杀价/拼团价/库存/最小购买)
|
* - 详情弹窗: 含 SKU 列表 (编码/组合/封面/原价/现价/秒杀价/拼团价/库存/最小购买)
|
||||||
* - 审核弹窗: 通过/驳回 + 审核意见
|
* - 审核弹窗: 通过/驳回 + 审核意见
|
||||||
@ -19,7 +19,11 @@
|
|||||||
import { ref, onMounted, computed, reactive } from 'vue'
|
import { ref, onMounted, computed, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
import { httpGet } from '@/utils/request'
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||||||
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
|
||||||
interface Good {
|
interface Good {
|
||||||
id: string
|
id: string
|
||||||
@ -105,6 +109,12 @@ const limit = ref(10)
|
|||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const selection = ref<Good[]>([])
|
const selection = ref<Good[]>([])
|
||||||
|
|
||||||
|
// 状态统计 (与 tab 解耦, 只跟搜索条件关联 - 跨 tab 一致)
|
||||||
|
const checkStats = reactive({ pending: 0, approved: 0, rejected: 0 })
|
||||||
|
|
||||||
|
// W17.6: 商品总数 + SKU 总数 (实物商品下, 全量, 不分 checkStatus)
|
||||||
|
const counts = reactive({ goodCount: 0, skuCount: 0 })
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
enterpriseName: '',
|
enterpriseName: '',
|
||||||
@ -125,74 +135,161 @@ const batchRejectDialogVisible = ref(false)
|
|||||||
const batchRejectForm = reactive({ reason: '' })
|
const batchRejectForm = reactive({ reason: '' })
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ type: 'selection' as const, width: 50 },
|
{ type: 'selection' as const, label: '', width: 50 },
|
||||||
{ prop: 'supplierGoodId', label: '商品ID', width: 140 },
|
{ prop: 'supplierGoodId', label: '商品ID', width: 100 },
|
||||||
{ prop: 'title', label: '商品名称', minWidth: 240, slot: 'title' },
|
{ prop: 'title', label: '商品名称', minWidth: 320, slot: 'title' },
|
||||||
{ prop: 'enterpriseName', label: '供应商', minWidth: 180, showOverflowTooltip: true },
|
{ prop: 'enterpriseName', label: '供应商', minWidth: 180, showOverflowTooltip: true },
|
||||||
{ prop: 'categoryName', label: '分类', minWidth: 240, slot: 'category' },
|
{ prop: 'categoryName', label: '分类', minWidth: 180, slot: 'category' },
|
||||||
{ prop: 'brandName', label: '品牌', width: 120 },
|
{ prop: 'brandName', label: '品牌', width: 110 },
|
||||||
{ prop: 'unitName', label: '单位', width: 80, align: 'center' as const },
|
{ prop: 'unitName', label: '单位', width: 90, minWidth: 90, align: 'center' as const },
|
||||||
{ prop: 'onShelf', label: '铺货', width: 100, align: 'center' as const, slot: 'onShelf' },
|
{ prop: 'onShelf', label: '铺货', width: 100, align: 'center' as const, slot: 'onShelf' },
|
||||||
{ prop: 'checkOpinion', label: '审核意见', minWidth: 160, showOverflowTooltip: true },
|
|
||||||
{ prop: 'checkStatus', label: '状态', width: 100, align: 'center' as const, slot: 'checkStatus' },
|
{ prop: 'checkStatus', label: '状态', width: 100, align: 'center' as const, slot: 'checkStatus' },
|
||||||
{ prop: 'createDate', label: '申请时间', width: 170 },
|
{ prop: 'createDate', label: '申请时间', width: 170 },
|
||||||
{ label: '操作', width: 240, align: 'center' as const, fixed: 'right' as const, slot: 'action' }
|
{ label: '操作', width: 140, align: 'center' as const, fixed: 'right' as const, slot: 'action' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async function buildSearchParamsForStats() {
|
||||||
|
const params: any = { type: 1 }
|
||||||
|
if (filters.title) params.title = filters.title
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const base = await buildSearchParamsForStats()
|
||||||
|
const [r1, r2, r3] = await Promise.all([
|
||||||
|
httpGet('/supplier/good/pageList', { ...base, checkStatus: 1, page: 1, limit: 1 }),
|
||||||
|
httpGet('/supplier/good/pageList', { ...base, checkStatus: 2, page: 1, limit: 1 }),
|
||||||
|
httpGet('/supplier/good/pageList', { ...base, checkStatus: 3, page: 1, limit: 1 })
|
||||||
|
])
|
||||||
|
checkStats.pending = Number(r1?.total ?? 0)
|
||||||
|
checkStats.approved = Number(r2?.total ?? 0)
|
||||||
|
checkStats.rejected = Number(r3?.total ?? 0)
|
||||||
|
} catch {
|
||||||
|
checkStats.pending = 0
|
||||||
|
checkStats.approved = 0
|
||||||
|
checkStats.rejected = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// W17.6: 全量商品数 + 全量 SKU 数 (实物, type=1, 不分 checkStatus / 搜索)
|
||||||
|
async function loadCounts() {
|
||||||
|
try {
|
||||||
|
const r: any = await httpGet('/supplier/good/sku-count', { type: 1 })
|
||||||
|
counts.goodCount = Number(r?.goodCount ?? 0)
|
||||||
|
counts.skuCount = Number(r?.skuCount ?? 0)
|
||||||
|
} catch {
|
||||||
|
counts.goodCount = 0
|
||||||
|
counts.skuCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
// 端点: /supplier/good/page 后端 404, 用 FALLBACK
|
// W13.1: /supplier/good/pageList 返完整字段 (16133 条) — 比 search 端点更全
|
||||||
await new Promise(r => setTimeout(r, 200))
|
const data = await httpGet<{ total: number | string; list: any[] }>('/supplier/good/pageList', {
|
||||||
let rows = [...FALLBACK]
|
page: page.value,
|
||||||
if (filters.title) rows = rows.filter(r => r.title.toLowerCase().includes(filters.title.toLowerCase()))
|
limit: limit.value,
|
||||||
|
type: 1, // 1=实物商品 (2=服务商品, 在 ServiceGoodListView 单独展示)
|
||||||
|
...(filters.title ? { title: filters.title } : {})
|
||||||
|
})
|
||||||
|
let rows = (data.list || []).map((it: any) => ({
|
||||||
|
id: String(it.id),
|
||||||
|
supplierGoodId: it.supplierGoodId || String(it.id),
|
||||||
|
icon: it.icon || null,
|
||||||
|
title: it.title || '--',
|
||||||
|
enterpriseName: it.enterpriseName || '--',
|
||||||
|
firstCategoryName: it.firstCategoryName || '',
|
||||||
|
secondCategoryName: it.secondCategoryName || '',
|
||||||
|
thirdCategoryName: it.thirdCategoryName || '',
|
||||||
|
brandName: it.brandName || '--',
|
||||||
|
unitName: it.unitName || '--',
|
||||||
|
onShelf: it.onShelf ?? 1,
|
||||||
|
checkOpinion: it.checkOpinion || '',
|
||||||
|
checkStatus: it.checkStatus ?? 2,
|
||||||
|
createDate: it.createDate || '',
|
||||||
|
raw: it
|
||||||
|
}))
|
||||||
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
|
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
|
||||||
if (filters.checkStatus) rows = rows.filter(r => String(r.checkStatus) === filters.checkStatus)
|
if (filters.checkStatus) rows = rows.filter(r => String(r.checkStatus) === filters.checkStatus)
|
||||||
list.value = rows
|
list.value = rows
|
||||||
total.value = rows.length
|
total.value = Number(data.total) || 0
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.msg || e?.message || '加载失败'
|
error.value = e?.msg || e?.message || '加载失败'
|
||||||
list.value = FALLBACK
|
list.value = []
|
||||||
total.value = FALLBACK.length
|
total.value = 0
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSearch() { page.value = 1; load() }
|
function onSearch() { page.value = 1; Promise.all([load(), loadStats(), loadCounts()]) }
|
||||||
function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; page.value = 1; load() }
|
function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; page.value = 1; Promise.all([load(), loadStats(), loadCounts()]) }
|
||||||
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: Good[]) { selection.value = rows }
|
function onSelectionChange(rows: Good[]) { selection.value = rows }
|
||||||
|
|
||||||
|
const tabItems = computed(() => [
|
||||||
|
// 全部 = 待审核 + 已通过 + 已驳回 (跨 tab 一致, 跟搜索条件关联)
|
||||||
|
{ key: '', label: '全部', count: checkStats.pending + checkStats.approved + checkStats.rejected },
|
||||||
|
{ key: '1', label: '待审核', count: checkStats.pending },
|
||||||
|
{ key: '2', label: '已通过', count: checkStats.approved },
|
||||||
|
{ key: '3', label: '已驳回', count: checkStats.rejected }
|
||||||
|
])
|
||||||
|
|
||||||
function onStatusTab(v: string) { filters.checkStatus = v; page.value = 1; load() }
|
function onStatusTab(v: string) { filters.checkStatus = v; page.value = 1; load() }
|
||||||
|
|
||||||
function onShowDetail(row: Good) { detailData.value = row; detailVisible.value = true }
|
function onShowDetail(row: Good) { detailData.value = row; detailVisible.value = true }
|
||||||
function onShowCheckDialog(row: Good) { checkData.value = row; checkForm.checkStatus = 2; checkForm.checkOpinion = ''; checkDialogVisible.value = true }
|
function onShowCheckDialog(row: Good) { checkData.value = row; checkForm.checkStatus = 2; checkForm.checkOpinion = ''; checkDialogVisible.value = true }
|
||||||
function onSubmitCheck() {
|
async function onSubmitCheck() {
|
||||||
if (!checkForm.checkOpinion.trim()) { ElMessage.warning('请填写审核意见'); return }
|
if (!checkForm.checkOpinion.trim()) { ElMessage.warning('请填写审核意见'); return }
|
||||||
ElMessage.success(checkForm.checkStatus === 2 ? '审核通过 (mock)' : '已驳回 (mock)')
|
// W14.3: 真实调 /supplier/good/checkGood?checkStatus=&checkOpinion=
|
||||||
|
try {
|
||||||
|
const id = checkData.value?.id || checkData.value?.raw?.id
|
||||||
|
const r: any = await httpGet('/supplier/good/checkGood', {
|
||||||
|
id,
|
||||||
|
checkStatus: checkForm.checkStatus,
|
||||||
|
checkOpinion: checkForm.checkOpinion || ''
|
||||||
|
})
|
||||||
|
if (r?.code === 0) {
|
||||||
|
ElMessage.success(checkForm.checkStatus === 2 ? '审核通过' : '已驳回')
|
||||||
checkDialogVisible.value = false
|
checkDialogVisible.value = false
|
||||||
load()
|
load()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(r?.msg || '操作失败')
|
||||||
}
|
}
|
||||||
function onOnShelf(row: Good) {
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
ElMessageBox.confirm(`确认${row.onShelf === 1 ? '下架' : '上架'}「${row.title}」?`, '提示', { type: 'warning' }).then(() => {
|
}
|
||||||
ElMessage.success('操作成功 (mock)')
|
async function onOnShelf(row: Good) {
|
||||||
load()
|
try { await ElMessageBox.confirm(`确认${row.onShelf === 1 ? '下架' : '上架'}「${row.title}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
}).catch(() => {})
|
try {
|
||||||
|
const r: any = await httpGet('/supplier/good/onShelfGood', { id: row.id })
|
||||||
|
if (r?.code === 0) { ElMessage.success(row.onShelf === 1 ? '已下架' : '已上架'); load() }
|
||||||
|
else ElMessage.error(r?.msg || '操作失败')
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
}
|
}
|
||||||
function onShowCategoryDialog(row: Good) { categoryData.value = row; categoryDialogVisible.value = true }
|
function onShowCategoryDialog(row: Good) { categoryData.value = row; categoryDialogVisible.value = true }
|
||||||
function onSubmitCategory() {
|
async function onSubmitCategory() {
|
||||||
ElMessage.success('分类已修改 (mock)')
|
try {
|
||||||
categoryDialogVisible.value = false
|
const r: any = await httpPost('/supplier/good/update', {
|
||||||
load()
|
id: categoryData.value?.id || categoryData.value?.raw?.id,
|
||||||
|
firstCategoryId: categoryForm.firstCategoryId || undefined,
|
||||||
|
secondCategoryId: categoryForm.secondCategoryId || undefined,
|
||||||
|
thirdCategoryId: categoryForm.thirdCategoryId || undefined
|
||||||
|
})
|
||||||
|
if (r?.code === 0) { ElMessage.success('分类已修改'); categoryDialogVisible.value = false; load() }
|
||||||
|
else ElMessage.error(r?.msg || '操作失败')
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
}
|
}
|
||||||
function onDelete(row: Good) {
|
async function onDelete(row: Good) {
|
||||||
ElMessageBox.confirm(`确认删除「${row.title}」?`, '提示', { type: 'warning' }).then(() => {
|
try { await ElMessageBox.confirm(`确认删除「${row.title}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
ElMessage.success('已删除 (mock)')
|
try {
|
||||||
load()
|
const r: any = await httpGet('/supplier/good/delete', { id: row.id })
|
||||||
}).catch(() => {})
|
if (r?.code === 0) { ElMessage.success('已删除'); load() }
|
||||||
|
else ElMessage.error(r?.msg || '操作失败')
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBatchCheck() {
|
function onBatchCheck() {
|
||||||
@ -235,50 +332,27 @@ function onDeleteBatch() {
|
|||||||
function onExport() { ElMessage.success('导出商品 (mock)') }
|
function onExport() { ElMessage.success('导出商品 (mock)') }
|
||||||
function onAdd() { ElMessage.info('新增商品 (mock)') }
|
function onAdd() { ElMessage.info('新增商品 (mock)') }
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(() => Promise.all([load(), loadStats(), loadCounts()]))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="商品管理" :sub="`共 ${counts.goodCount} 个商品,${counts.skuCount} 个SKU`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">商品管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个商品</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onExport">导出</el-button>
|
<el-button @click="onExport">导出</el-button>
|
||||||
<el-button type="primary" @click="onAdd">新增商品</el-button>
|
<el-button type="primary" @click="onAdd">新增商品</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索区 -->
|
<ListTabs v-model="filters.checkStatus" :items="tabItems" @change="onStatusTab" />
|
||||||
<div class="rr-card rr-search">
|
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
|
||||||
<el-form-item label="商品名称">
|
|
||||||
<el-input v-model="filters.title" placeholder="请输入商品名称" clearable style="width: 200px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="供应商">
|
|
||||||
<el-input v-model="filters.enterpriseName" placeholder="供应商名称" clearable style="width: 200px" />
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- 状态 Tab -->
|
<SearchBar
|
||||||
<div class="rr-tabs-row">
|
v-model:filters="filters"
|
||||||
<div class="rr-tabs">
|
:fields="[
|
||||||
<div
|
{ key: 'title', label: '商品名称', type: 'text', placeholder: '请输入商品名称' },
|
||||||
v-for="t in STATUS_TABS"
|
{ key: 'enterpriseName', label: '供应商', type: 'text', placeholder: '供应商名称' }
|
||||||
:key="t.value"
|
]"
|
||||||
class="rr-tab"
|
@search="onSearch"
|
||||||
:class="{ active: filters.checkStatus === t.value }"
|
@reset="onReset"
|
||||||
@click="onStatusTab(t.value)"
|
/>
|
||||||
>{{ t.label }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 批量操作 -->
|
<!-- 批量操作 -->
|
||||||
<div class="rr-batch-bar" v-if="selection.length > 0">
|
<div class="rr-batch-bar" v-if="selection.length > 0">
|
||||||
@ -290,8 +364,62 @@ onMounted(load)
|
|||||||
<el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button>
|
<el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 (用 DataTable 套裸 el-table, 复用分页 + :borderless + :page-sizes + el-result 错误展示) -->
|
||||||
<div class="rr-card">
|
<DataTable
|
||||||
|
:columns="columns"
|
||||||
|
:data="list"
|
||||||
|
:loading="loading"
|
||||||
|
:total="total"
|
||||||
|
:page="page"
|
||||||
|
:limit="limit"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:show-pagination="true"
|
||||||
|
:error="error"
|
||||||
|
empty-text="暂无商品"
|
||||||
|
:borderless="true"
|
||||||
|
row-key="id"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
@limit-change="onLimitChange"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
|
>
|
||||||
|
<template #title="{ row }">
|
||||||
|
<div class="rr-good-cell">
|
||||||
|
<div v-if="row.icon" class="rr-good-icon">
|
||||||
|
<el-image :src="row.icon" :preview-src-list="[row.icon]" fit="cover" style="width: 40px; height: 40px; border-radius: 4px" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div>
|
||||||
|
<span class="rr-good-title">{{ row.title }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #category="{ row }">
|
||||||
|
<span>{{ [row.firstCategoryName, row.secondCategoryName, row.thirdCategoryName].filter(Boolean).join(' / ') || '--' }}</span>
|
||||||
|
</template>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableActions
|
||||||
|
:row="row"
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'primary', onClick: onShowDetail },
|
||||||
|
{ label: '审核', type: 'primary', when: (r: any) => r.checkStatus === 1, onClick: onShowCheckDialog },
|
||||||
|
{ label: (r: any) => r.onShelf === 1 ? '下架' : '上架', type: (r: any) => r.onShelf === 1 ? 'warning' : 'success', onClick: onOnShelf },
|
||||||
|
{ label: '改分类', type: 'primary', onClick: onShowCategoryDialog },
|
||||||
|
{ label: '删除', type: 'danger', divided: true, onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<!-- 原 el-table 区域 (暂时保留作为参考, 实际由 DataTable 接管) -->
|
||||||
|
<div v-if="false" class="rr-card">
|
||||||
<el-table
|
<el-table
|
||||||
:data="list"
|
:data="list"
|
||||||
border
|
border
|
||||||
@ -349,17 +477,6 @@ onMounted(load)
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详情弹窗 (含 SKU 列表, 沿用老 vue 弹窗布局) -->
|
<!-- 详情弹窗 (含 SKU 列表, 沿用老 vue 弹窗布局) -->
|
||||||
@ -484,21 +601,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1500px; }
|
.rr-page { max-width: 1500px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.rr-search :deep(.el-form-item) { margin-bottom: 0; margin-right: 12px; }
|
|
||||||
.rr-tabs-row { margin-bottom: 16px; display: flex; align-items: center; }
|
|
||||||
.rr-tabs { display: flex; gap: 4px; padding: 4px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); }
|
|
||||||
.rr-tab { padding: 6px 16px; border-radius: var(--radius); font-size: 13px; color: var(--ink-2); cursor: pointer; transition: all 0.15s; }
|
|
||||||
.rr-tab:hover { color: var(--ink); background: var(--bg-soft); }
|
|
||||||
.rr-tab.active { color: white; background: var(--primary); font-weight: 500; }
|
|
||||||
.rr-batch-bar { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: var(--bg-soft); border-radius: var(--radius); margin-bottom: 12px; }
|
.rr-batch-bar { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: var(--bg-soft); border-radius: var(--radius); margin-bottom: 12px; }
|
||||||
.rr-batch-tip { font-size: 13px; color: var(--ink-2); margin-right: 8px; }
|
.rr-batch-tip { font-size: 13px; color: var(--ink-2); margin-right: 8px; }
|
||||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
|
||||||
.rr-good-cell { display: flex; align-items: center; gap: 10px; }
|
.rr-good-cell { display: flex; align-items: center; gap: 10px; }
|
||||||
.rr-good-icon-placeholder { width: 40px; height: 40px; border-radius: 4px; background: var(--bg-soft); display: flex; align-items: center; justify-content: center; color: var(--ink-3); font-weight: 600; flex-shrink: 0; }
|
.rr-good-icon-placeholder { width: 40px; height: 40px; border-radius: 4px; background: var(--bg-soft); display: flex; align-items: center; justify-content: center; color: var(--ink-3); font-weight: 600; flex-shrink: 0; }
|
||||||
.rr-good-title { font-size: 13.5px; color: var(--ink); }
|
.rr-good-title { font-size: 13.5px; color: var(--ink); }
|
||||||
|
|||||||
116
src/modules/supplier/historylog/HistorylogListView.vue
Normal file
116
src/modules/supplier/historylog/HistorylogListView.vue
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 浏览足迹 - W16
|
||||||
|
*
|
||||||
|
* 沿用老 vue supplier/historylog.vue 字段
|
||||||
|
*
|
||||||
|
* 端点: 4 URL 探测 (historylog/page, /list, /search, /queryPage); 404 → FALLBACK seed
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
|
interface HistoryRow { id:string; userId:string; userType:number; goodType:number; supplierGoodId:string; createTime:string }
|
||||||
|
const ENDPOINTS = [
|
||||||
|
'/supplier/historylog/page',
|
||||||
|
'/supplier/historylog/list',
|
||||||
|
'/supplier/historylog/search',
|
||||||
|
'/supplier/historylog/queryPage'
|
||||||
|
]
|
||||||
|
const FALLBACK: HistoryRow[] = [
|
||||||
|
{ id:'H001', userId:'u10086', userType:1, goodType:1, supplierGoodId:'SG1001', createTime:'2026-07-04 14:22:11' },
|
||||||
|
{ id:'H002', userId:'u10112', userType:1, goodType:2, supplierGoodId:'SG1002', createTime:'2026-07-05 09:45:30' },
|
||||||
|
{ id:'H003', userId:'u10133', userType:2, goodType:1, supplierGoodId:'SG1003', createTime:'2026-07-05 11:18:42' },
|
||||||
|
{ id:'H004', userId:'u10144', userType:1, goodType:1, supplierGoodId:'SG1004', createTime:'2026-07-05 16:02:55' },
|
||||||
|
{ id:'H005', userId:'u10155', userType:2, goodType:2, supplierGoodId:'SG1005', createTime:'2026-07-06 08:30:18' },
|
||||||
|
{ id:'H006', userId:'u10166', userType:1, goodType:1, supplierGoodId:'SG1006', createTime:'2026-07-06 10:55:08' },
|
||||||
|
{ id:'H007', userId:'u10177', userType:1, goodType:2, supplierGoodId:'SG1007', createTime:'2026-07-06 13:42:35' },
|
||||||
|
{ id:'H008', userId:'u10188', userType:2, goodType:1, supplierGoodId:'SG1008', createTime:'2026-07-06 15:18:42' },
|
||||||
|
{ id:'H009', userId:'u10199', userType:1, goodType:1, supplierGoodId:'SG1009', createTime:'2026-07-06 17:33:21' },
|
||||||
|
{ id:'H010', userId:'u10210', userType:1, goodType:2, supplierGoodId:'SG1010', createTime:'2026-07-06 19:08:55' },
|
||||||
|
{ id:'H011', userId:'u10221', userType:2, goodType:1, supplierGoodId:'SG1011', createTime:'2026-07-07 08:22:11' },
|
||||||
|
{ id:'H012', userId:'u10232', userType:1, goodType:1, supplierGoodId:'SG1012', createTime:'2026-07-07 09:48:30' },
|
||||||
|
{ id:'H013', userId:'u10243', userType:1, goodType:2, supplierGoodId:'SG1013', createTime:'2026-07-07 11:25:48' },
|
||||||
|
{ id:'H014', userId:'u10254', userType:2, goodType:1, supplierGoodId:'SG1014', createTime:'2026-07-07 13:42:11' }
|
||||||
|
]
|
||||||
|
type Col = { type?: 'status'|'text'|'selection'; prop?: string; label: string; width?: number; slot?: string; fixed?: 'right'|true }
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ type:'selection', label:'', width:50 },
|
||||||
|
{ prop:'id', label:'ID', width:90, fixed:true },
|
||||||
|
{ prop:'userId', label:'用户ID', width:110 },
|
||||||
|
{ prop:'userType', label:'用户类型', width:100, slot:'userType' },
|
||||||
|
{ prop:'goodType', label:'商品类型', width:100, slot:'goodType' },
|
||||||
|
{ prop:'supplierGoodId', label:'商品ID', width:130 },
|
||||||
|
{ prop:'createTime', label:'浏览时间', width:180, slot:'time' },
|
||||||
|
{ prop:'actions', label:'操作', width:150, slot:'actions', fixed:'right' }
|
||||||
|
]
|
||||||
|
const STATUS_DEFAULTS = [
|
||||||
|
{ value:'', label:'全部' },
|
||||||
|
{ value:'1', label:'C端用户' },
|
||||||
|
{ value:'2', label:'供应商' }
|
||||||
|
]
|
||||||
|
const list = ref<HistoryRow[]>([]); const total = ref(0); const loading = ref(false); const page = ref(1); const limit = ref(10); const filterKw = ref(''); const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api'|'mock'|'loading'>('loading'); const apiError = ref<string>('')
|
||||||
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet<any>(ep, { page: page.value, limit: limit.value })
|
||||||
|
const rows = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows; total.value = Number(data?.total || rows.length); dataSource.value = 'api'; apiError.value = ''; loading.value = false; return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]; total.value = FALLBACK.length; dataSource.value = 'mock'; apiError.value = '后端 API 暂未实装, 已加载前端示例数据'; loading.value = false
|
||||||
|
}
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase(); let rs = list.value
|
||||||
|
if (statusTab.value) rs = rs.filter(r => String(r.userType) === statusTab.value)
|
||||||
|
if (kw) rs = rs.filter(r => String(r.id).includes(kw) || String(r.userId).includes(kw) || String(r.supplierGoodId).includes(kw))
|
||||||
|
return rs
|
||||||
|
})
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [{ key:'id', label:'ID', type:'text' },{ key:'userId', label:'用户ID', type:'text' },{ key:'supplierGoodId', label:'商品ID', type:'text' }]
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() { filterKw.value = String(filters.id || filters.userId || filters.supplierGoodId || ''); page.value = 1 }
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
async function onDelete(row: HistoryRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认删除「${row.id}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
ElMessage.success('已删除 (前端示例)')
|
||||||
|
}
|
||||||
|
function onView(row: HistoryRow) { ElMessage.info(`详情 ${row.id}`) }
|
||||||
|
function onExport() { ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)') }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="浏览足迹" sub="用户浏览商品行为记录" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_DEFAULTS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
<DataTable :columns="columns" :data="filtered"
|
||||||
|
:borderless="true" :loading="loading" :page="page" :limit="limit" :total="total" row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }" @limit-change="(l: number) => { limit = l; page = 1; load() }">
|
||||||
|
<template #userType="{ row }">
|
||||||
|
<el-tag :type="row.userType === 1 ? 'primary' : 'success'" size="small">{{ row.userType === 1 ? 'C端用户' : '供应商' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #goodType="{ row }">
|
||||||
|
<el-tag :type="row.goodType === 1 ? 'warning' : 'info'" size="small">{{ row.goodType === 1 ? '实物' : '服务' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #time="{ row }">{{ row.createTime }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<el-button type="primary" link @click="onView(row)">详情</el-button>
|
||||||
|
<el-button type="danger" link @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
117
src/modules/supplier/logistics/LogisticsListView.vue
Normal file
117
src/modules/supplier/logistics/LogisticsListView.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 物流方案管理 - W16
|
||||||
|
*
|
||||||
|
* 沿用老 vue supplier/logistics.vue 字段:
|
||||||
|
* - 选择 + id + enterpriseId + isDefault + logisticsName + chargeType + isOpen + createDate
|
||||||
|
*
|
||||||
|
* 端点: /supplier/logistics/page 探测; 404 → FALLBACK seed (12 条)
|
||||||
|
*
|
||||||
|
* isDefault 0否1是 / chargeType 1=按件数 2=整单 / isOpen 0否1是
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
|
interface LogisticsRow {
|
||||||
|
id: string; enterpriseId: string; isDefault: number; logisticsName: string; chargeType: number; isOpen: number; createDate: string
|
||||||
|
}
|
||||||
|
const DEFAULT_L: Record<number, string> = { 0: '否', 1: '是' }
|
||||||
|
const CHARGE_L: Record<number, string> = { 1: '按件数', 2: '整单' }
|
||||||
|
const ENDPOINTS = ['/supplier/logistics/page', '/supplier/logistics/list', '/supplier/logistics/search', '/supplier/logistics/queryPage']
|
||||||
|
const FALLBACK: LogisticsRow[] = [
|
||||||
|
{ id:'L001', enterpriseId:'s20015', isDefault:1, logisticsName:'标准配送 (同城48h)', chargeType:1, isOpen:1, createDate:'2026-04-12 09:30:00' },
|
||||||
|
{ id:'L002', enterpriseId:'s20016', isDefault:1, logisticsName:'经济配送 (跨省3-5天)', chargeType:2, isOpen:1, createDate:'2026-04-15 14:22:00' },
|
||||||
|
{ id:'L003', enterpriseId:'s20017', isDefault:0, logisticsName:'加急配送 (同城当日达)', chargeType:1, isOpen:1, createDate:'2026-04-20 16:48:00' },
|
||||||
|
{ id:'L004', enterpriseId:'s20018', isDefault:1, logisticsName:'大宗货运专线', chargeType:2, isOpen:1, createDate:'2026-05-02 10:15:00' },
|
||||||
|
{ id:'L005', enterpriseId:'s20019', isDefault:0, logisticsName:'仓配一体化 (本地仓发货)', chargeType:1, isOpen:0, createDate:'2026-05-08 11:35:00' },
|
||||||
|
{ id:'L006', enterpriseId:'s20020', isDefault:1, logisticsName:'冷链配送 (8-15°C)', chargeType:2, isOpen:1, createDate:'2026-05-14 08:55:00' },
|
||||||
|
{ id:'L007', enterpriseId:'s20021', isDefault:0, logisticsName:'大件专线 (超长件)', chargeType:2, isOpen:1, createDate:'2026-05-20 13:18:00' },
|
||||||
|
{ id:'L008', enterpriseId:'s20022', isDefault:1, logisticsName:'厂家直发 (48-72h)', chargeType:1, isOpen:1, createDate:'2026-06-02 09:42:00' },
|
||||||
|
{ id:'L009', enterpriseId:'s20023', isDefault:0, logisticsName:'节假日专送', chargeType:1, isOpen:0, createDate:'2026-06-10 16:08:00' },
|
||||||
|
{ id:'L010', enterpriseId:'s20024', isDefault:1, logisticsName:'海外集运', chargeType:2, isOpen:1, createDate:'2026-06-18 11:25:00' },
|
||||||
|
{ id:'L011', enterpriseId:'s20025', isDefault:0, logisticsName:'安装配送一体', chargeType:1, isOpen:1, createDate:'2026-06-28 14:38:00' },
|
||||||
|
{ id:'L012', enterpriseId:'s20026', isDefault:0, logisticsName:'试用物流 (10km内免费)', chargeType:1, isOpen:0, createDate:'2026-07-01 09:50:00' }
|
||||||
|
]
|
||||||
|
type Col = { type?: 'status' | 'text' | 'selection'; prop?: string; label: string; width?: number; slot?: string; fixed?: 'right' | true }
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ type: 'selection', label: '', width: 50 },
|
||||||
|
{ prop: 'id', label: '方案ID', width: 100, fixed: true },
|
||||||
|
{ prop: 'enterpriseId', label: '所属企业ID', width: 130 },
|
||||||
|
{ type: 'status', prop: 'isDefault', label: '是否默认', width: 90 },
|
||||||
|
{ prop: 'logisticsName', label: '方案名称', width: 220 },
|
||||||
|
{ type: 'status', prop: 'chargeType', label: '计费方式', width: 110 },
|
||||||
|
{ type: 'status', prop: 'isOpen', label: '是否启用', width: 90 },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 160, slot: 'time' },
|
||||||
|
{ prop: 'actions', label: '操作', width: 200, slot: 'actions', fixed: 'right' }
|
||||||
|
]
|
||||||
|
const STATUS_DEFAULTS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '1', label: '默认' },
|
||||||
|
{ value: '0', label: '非默认' }
|
||||||
|
]
|
||||||
|
const list = ref<LogisticsRow[]>([]); const total = ref(0); const loading = ref(false); const page = ref(1); const limit = ref(10); const filterKw = ref(''); const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api' | 'mock' | 'loading'>('loading'); const apiError = ref<string>('')
|
||||||
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet<any>(ep, { page: page.value, limit: limit.value })
|
||||||
|
const rows = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows; total.value = Number(data?.total || rows.length); dataSource.value = 'api'; apiError.value = ''; loading.value = false; return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]; total.value = FALLBACK.length; dataSource.value = 'mock'; apiError.value = '后端 API 暂未实装, 已加载前端示例数据'; loading.value = false
|
||||||
|
}
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase(); let rs = list.value
|
||||||
|
if (statusTab.value) rs = rs.filter(r => String(r.isDefault) === statusTab.value)
|
||||||
|
if (kw) rs = rs.filter(r => String(r.id).toLowerCase().includes(kw) || String(r.logisticsName).toLowerCase().includes(kw) || String(r.enterpriseId).toLowerCase().includes(kw))
|
||||||
|
return rs
|
||||||
|
})
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [
|
||||||
|
{ key: 'id', label: '方案ID', type: 'text' },
|
||||||
|
{ key: 'logisticsName', label: '方案名称', type: 'text' },
|
||||||
|
{ key: 'enterpriseId', label: '所属企业ID', type: 'text' }
|
||||||
|
]
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() {
|
||||||
|
filterKw.value = String(filters.id || filters.logisticsName || filters.enterpriseId || ''); page.value = 1
|
||||||
|
}
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
async function onDelete(row: LogisticsRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认删除「${row.logisticsName}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
ElMessage.success('已删除 (前端示例)')
|
||||||
|
}
|
||||||
|
function onView(row: LogisticsRow) { ElMessage.info(`打开详情 ${row.id}`) }
|
||||||
|
function onExport() { ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)') }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="物流方案管理" sub="供应商物流配送方案配置" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_DEFAULTS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
<DataTable :columns="columns" :data="filtered"
|
||||||
|
:borderless="true" :loading="loading" :page="page" :limit="limit" :total="total" row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }" @limit-change="(l: number) => { limit = l; page = 1; load() }">
|
||||||
|
<template #time="{ row }">{{ row.createDate }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<el-button type="primary" link @click="onView(row)">查看</el-button>
|
||||||
|
<el-button type="danger" link @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
128
src/modules/supplier/ordercomment/OrderCommentListView.vue
Normal file
128
src/modules/supplier/ordercomment/OrderCommentListView.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 订单评价管理 - W16
|
||||||
|
*
|
||||||
|
* 沿用老 vue supplier/ordercomment.vue 字段
|
||||||
|
*
|
||||||
|
* 端点: 4 URL 探测 (ordercomment/page, /list, /search, /queryPage); 404 → FALLBACK seed
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
|
interface CommentRow { id:string; userId:string; parentId:string; goodType:number; skuDetailId:string; orderGoodId:string; supplierId:string; supplierGoodId:string; star:number; content:string; createTime:string }
|
||||||
|
const ENDPOINTS = [
|
||||||
|
'/supplier/ordercomment/page',
|
||||||
|
'/supplier/ordercomment/list',
|
||||||
|
'/supplier/ordercomment/search',
|
||||||
|
'/supplier/ordercomment/queryPage'
|
||||||
|
]
|
||||||
|
const FALLBACK: CommentRow[] = [
|
||||||
|
{ id:'OC001', userId:'u10086', parentId:'', goodType:1, skuDetailId:'SKU101', orderGoodId:'OG201', supplierId:'s20015', supplierGoodId:'SG101', star:5, content:'商品质量很好, 配送也快, 满意!', createTime:'2026-07-01 14:32:11' },
|
||||||
|
{ id:'OC002', userId:'u10112', parentId:'', goodType:1, skuDetailId:'SKU102', orderGoodId:'OG202', supplierId:'s20016', supplierGoodId:'SG102', star:4, content:'包装一般, 商品本身没问题', createTime:'2026-07-02 09:18:42' },
|
||||||
|
{ id:'OC003', userId:'u10133', parentId:'OC001', goodType:1, skuDetailId:'SKU103', orderGoodId:'OG203', supplierId:'s20017', supplierGoodId:'SG103', star:5, content:'感谢您的好评!', createTime:'2026-07-02 16:45:00' },
|
||||||
|
{ id:'OC004', userId:'u10144', parentId:'', goodType:2, skuDetailId:'SKU104', orderGoodId:'OG204', supplierId:'s20018', supplierGoodId:'SG104', star:5, content:'安装师傅态度很好', createTime:'2026-07-03 11:02:18' },
|
||||||
|
{ id:'OC005', userId:'u10155', parentId:'', goodType:1, skuDetailId:'SKU105', orderGoodId:'OG205', supplierId:'s20019', supplierGoodId:'SG105', star:4, content:'规格与描述相符', createTime:'2026-07-03 15:25:30' },
|
||||||
|
{ id:'OC006', userId:'u10166', parentId:'OC002', goodType:1, skuDetailId:'SKU106', orderGoodId:'OG206', supplierId:'s20016', supplierGoodId:'SG102', star:5, content:'已改进包装, 感谢建议', createTime:'2026-07-04 09:12:55' },
|
||||||
|
{ id:'OC007', userId:'u10177', parentId:'', goodType:1, skuDetailId:'SKU107', orderGoodId:'OG207', supplierId:'s20020', supplierGoodId:'SG107', star:4, content:'整体满意', createTime:'2026-07-04 16:48:22' },
|
||||||
|
{ id:'OC008', userId:'u10188', parentId:'', goodType:2, skuDetailId:'SKU108', orderGoodId:'OG208', supplierId:'s20021', supplierGoodId:'SG108', star:5, content:'量房很专业', createTime:'2026-07-05 08:30:18' },
|
||||||
|
{ id:'OC009', userId:'u10199', parentId:'', goodType:1, skuDetailId:'SKU109', orderGoodId:'OG209', supplierId:'s20022', supplierGoodId:'SG109', star:3, content:'颜色有偏差, 退换顺利', createTime:'2026-07-05 13:25:48' },
|
||||||
|
{ id:'OC010', userId:'u10210', parentId:'', goodType:1, skuDetailId:'SKU110', orderGoodId:'OG210', supplierId:'s20023', supplierGoodId:'SG110', star:5, content:'送货快', createTime:'2026-07-05 17:33:21' },
|
||||||
|
{ id:'OC011', userId:'u10221', parentId:'OC009', goodType:1, skuDetailId:'SKU111', orderGoodId:'OG211', supplierId:'s20022', supplierGoodId:'SG109', star:5, content:'补发正确颜色', createTime:'2026-07-06 09:42:18' },
|
||||||
|
{ id:'OC012', userId:'u10232', parentId:'', goodType:1, skuDetailId:'SKU112', orderGoodId:'OG212', supplierId:'s20024', supplierGoodId:'SG112', star:4, content:'批量优惠可以, 物流稍慢', createTime:'2026-07-06 14:22:11' },
|
||||||
|
{ id:'OC013', userId:'u10243', parentId:'', goodType:2, skuDetailId:'SKU113', orderGoodId:'OG213', supplierId:'s20025', supplierGoodId:'SG113', star:5, content:'自助下单良好', createTime:'2026-07-06 19:08:55' },
|
||||||
|
{ id:'OC014', userId:'u10254', parentId:'', goodType:1, skuDetailId:'SKU114', orderGoodId:'OG214', supplierId:'s20026', supplierGoodId:'SG114', star:5, content:'商品不错, 配送好', createTime:'2026-07-07 09:25:48' },
|
||||||
|
{ id:'OC015', userId:'u10265', parentId:'', goodType:1, skuDetailId:'SKU115', orderGoodId:'OG215', supplierId:'s20015', supplierGoodId:'SG101', star:3, content:'便宜但慢', createTime:'2026-07-07 13:42:11' }
|
||||||
|
]
|
||||||
|
type Col = { type?: 'status'|'text'|'selection'; prop?: string; label: string; width?: number; slot?: string; fixed?: 'right'|true }
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ type:'selection', label:'', width:50 },
|
||||||
|
{ prop:'id', label:'评价ID', width:100, fixed:true },
|
||||||
|
{ prop:'orderGoodId', label:'小订单ID', width:130 },
|
||||||
|
{ prop:'supplierGoodId', label:'商品ID', width:130 },
|
||||||
|
{ prop:'userId', label:'评价人', width:110 },
|
||||||
|
{ prop:'star', label:'评分', width:80, slot:'star' },
|
||||||
|
{ prop:'content', label:'内容', width:280, slot:'content' },
|
||||||
|
{ prop:'createTime', label:'时间', width:160, slot:'time' },
|
||||||
|
{ prop:'actions', label:'操作', width:140, slot:'actions', fixed:'right' }
|
||||||
|
]
|
||||||
|
const STATUS_DEFAULTS = [
|
||||||
|
{ value:'', label:'全部' },
|
||||||
|
{ value:'5', label:'5星' },
|
||||||
|
{ value:'4', label:'4星' },
|
||||||
|
{ value:'3', label:'3星' },
|
||||||
|
{ value:'1', label:'≤2星' }
|
||||||
|
]
|
||||||
|
const list = ref<CommentRow[]>([]); const total = ref(0); const loading = ref(false); const page = ref(1); const limit = ref(10); const filterKw = ref(''); const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api'|'mock'|'loading'>('loading'); const apiError = ref<string>('')
|
||||||
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet<any>(ep, { page: page.value, limit: limit.value })
|
||||||
|
const rows = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows; total.value = Number(data?.total || rows.length); dataSource.value = 'api'; apiError.value = ''; loading.value = false; return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]; total.value = FALLBACK.length; dataSource.value = 'mock'; apiError.value = '后端 API 暂未实装, 已加载前端示例数据'; loading.value = false
|
||||||
|
}
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase(); let rs = list.value
|
||||||
|
if (statusTab.value) { if (statusTab.value === '1') rs = rs.filter(r => r.star < 3); else rs = rs.filter(r => String(r.star) === statusTab.value) }
|
||||||
|
if (kw) rs = rs.filter(r => String(r.id).includes(kw) || String(r.content).includes(kw) || String(r.userId).includes(kw))
|
||||||
|
return rs
|
||||||
|
})
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [{ key:'id', label:'评价ID', type:'text' },{ key:'userId', label:'评价人ID', type:'text' },{ key:'content', label:'内容关键词', type:'text' }]
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() { filterKw.value = String(filters.id || filters.userId || filters.content || ''); page.value = 1 }
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
async function onDelete(row: CommentRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认删除评价「${row.id}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
ElMessage.success('已删除 (前端示例)')
|
||||||
|
}
|
||||||
|
function onReply(row: CommentRow) { ElMessage.info(`回复 ${row.id}`) }
|
||||||
|
function onView(row: CommentRow) { ElMessage.info(`详情 ${row.id}`) }
|
||||||
|
function onExport() { ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)') }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="订单评价管理" sub="订单商品的用户评价管理" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_DEFAULTS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
<DataTable :columns="columns" :data="filtered"
|
||||||
|
:borderless="true" :loading="loading" :page="page" :limit="limit" :total="total" row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }" @limit-change="(l: number) => { limit = l; page = 1; load() }">
|
||||||
|
<template #star="{ row }">
|
||||||
|
<el-rate v-model="row.star" :max="5" disabled />
|
||||||
|
</template>
|
||||||
|
<template #content="{ row }">
|
||||||
|
<span :title="row.content">{{ (row.content || '').slice(0, 28) }}{{ (row.content || '').length > 28 ? '...' : '' }}</span>
|
||||||
|
</template>
|
||||||
|
<template #time="{ row }">{{ row.createTime }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableActions
|
||||||
|
:row="row"
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'primary', onClick: onView },
|
||||||
|
{ label: '回复', type: 'primary', when: (r: any) => !r.parentId, onClick: onReply },
|
||||||
|
{ label: '删除', type: 'danger', onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
122
src/modules/supplier/promoter/PromoterListView.vue
Normal file
122
src/modules/supplier/promoter/PromoterListView.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 推广员管理 - W16
|
||||||
|
*
|
||||||
|
* 沿用老 vue supplier/promoter.vue 字段
|
||||||
|
*
|
||||||
|
* 端点: 4 URL 探测 (promoter/page, /list, /search, /queryPage); 404 → FALLBACK seed
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
|
interface PromoterRow { id:string; enterpriseId:string; enterpriseName:string; promoterName:string; promoterRadio:number; promoterGood:string; beginTime:string; endTime:string; createTime:string }
|
||||||
|
const ENDPOINTS = [
|
||||||
|
'/supplier/promoter/page',
|
||||||
|
'/supplier/promoter/list',
|
||||||
|
'/supplier/promoter/search',
|
||||||
|
'/supplier/promoter/queryPage'
|
||||||
|
]
|
||||||
|
const FALLBACK: PromoterRow[] = [
|
||||||
|
{ id:'P001', enterpriseId:'s20015', enterpriseName:'坚美建筑材料有限公司', promoterName:'张推广', promoterRadio:0.05, promoterGood:'国标槽钢/角钢系列', beginTime:'2026-04-01', endTime:'2026-12-31', createTime:'2026-04-01 09:30:00' },
|
||||||
|
{ id:'P002', enterpriseId:'s20016', enterpriseName:'华建门窗厂', promoterName:'李经纪人', promoterRadio:0.08, promoterGood:'断桥铝门窗', beginTime:'2026-04-15', endTime:'2026-10-30', createTime:'2026-04-15 14:22:00' },
|
||||||
|
{ id:'P003', enterpriseId:'s20017', enterpriseName:'融鑫管业', promoterName:'王工长', promoterRadio:0.06, promoterGood:'PPR给水管', beginTime:'2026-05-01', endTime:'2026-12-31', createTime:'2026-05-01 11:08:30' },
|
||||||
|
{ id:'P004', enterpriseId:'s20018', enterpriseName:'东方五金机械', promoterName:'赵渠道', promoterRadio:0.10, promoterGood:'电动工具', beginTime:'2026-05-10', endTime:'2026-09-30', createTime:'2026-05-10 16:48:00' },
|
||||||
|
{ id:'P005', enterpriseId:'s20019', enterpriseName:'美洁涂料', promoterName:'陈渠道', promoterRadio:0.07, promoterGood:'环保涂料', beginTime:'2026-05-20', endTime:'2026-12-31', createTime:'2026-05-20 09:15:00' },
|
||||||
|
{ id:'P006', enterpriseId:'s20020', enterpriseName:'万家灯具', promoterName:'周照明师', promoterRadio:0.12, promoterGood:'LED灯具', beginTime:'2026-06-01', endTime:'2026-12-31', createTime:'2026-06-01 10:42:00' },
|
||||||
|
{ id:'P007', enterpriseId:'s20021', enterpriseName:'顺达物流', promoterName:'吴同城', promoterRadio:0.05, promoterGood:'同城配送服务', beginTime:'2026-06-10', endTime:'2026-12-31', createTime:'2026-06-10 13:18:00' },
|
||||||
|
{ id:'P008', enterpriseId:'s20022', enterpriseName:'华建门窗厂', promoterName:'林工长', promoterRadio:0.08, promoterGood:'铝合金门窗', beginTime:'2026-06-20', endTime:'2026-12-31', createTime:'2026-06-20 11:35:00' },
|
||||||
|
{ id:'P009', enterpriseId:'s20023', enterpriseName:'众成厨具', promoterName:'黄销售', promoterRadio:0.06, promoterGood:'厨房用品', beginTime:'2026-07-01', endTime:'2026-12-31', createTime:'2026-07-01 09:50:00' },
|
||||||
|
{ id:'P010', enterpriseId:'s20024', enterpriseName:'东方五金机械', promoterName:'何专业', promoterRadio:0.09, promoterGood:'角磨机', beginTime:'2026-07-05', endTime:'2026-12-31', createTime:'2026-07-05 14:25:00' },
|
||||||
|
{ id:'P011', enterpriseId:'s20025', enterpriseName:'坚美建筑材料', promoterName:'钱工长', promoterRadio:0.07, promoterGood:'国标钢材', beginTime:'2026-07-08', endTime:'2026-12-31', createTime:'2026-07-08 11:42:00' },
|
||||||
|
{ id:'P012', enterpriseId:'s20026', enterpriseName:'美洁涂料', promoterName:'孙工长', promoterRadio:0.06, promoterGood:'外墙腻子粉', beginTime:'2026-07-12', endTime:'2026-12-31', createTime:'2026-07-12 16:08:00' }
|
||||||
|
]
|
||||||
|
type Col = { type?: 'status'|'text'|'selection'; prop?: string; label: string; width?: number; slot?: string; fixed?: 'right'|true }
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ type:'selection', label:'', width:50 },
|
||||||
|
{ prop:'id', label:'推广员ID', width:100, fixed:true },
|
||||||
|
{ prop:'enterpriseName', label:'所属供应商', width:200 },
|
||||||
|
{ prop:'promoterName', label:'推广员姓名', width:120 },
|
||||||
|
{ prop:'promoterRadio', label:'返佣比例', width:110, slot:'radio' },
|
||||||
|
{ prop:'promoterGood', label:'推广商品', width:200 },
|
||||||
|
{ prop:'beginTime', label:'开始时间', width:120, slot:'date1' },
|
||||||
|
{ prop:'endTime', label:'结束时间', width:120, slot:'date2' },
|
||||||
|
{ prop:'createTime', label:'创建时间', width:160, slot:'time' },
|
||||||
|
{ prop:'actions', label:'操作', width:140, slot:'actions', fixed:'right' }
|
||||||
|
]
|
||||||
|
const STATUS_DEFAULTS = [
|
||||||
|
{ value:'', label:'全部' },
|
||||||
|
{ value:'1', label:'推广中' },
|
||||||
|
{ value:'0', label:'已结束' }
|
||||||
|
]
|
||||||
|
const list = ref<PromoterRow[]>([]); const total = ref(0); const loading = ref(false); const page = ref(1); const limit = ref(10); const filterKw = ref(''); const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api'|'mock'|'loading'>('loading'); const apiError = ref<string>('')
|
||||||
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet<any>(ep, { page: page.value, limit: limit.value })
|
||||||
|
const rows = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows; total.value = Number(data?.total || rows.length); dataSource.value = 'api'; apiError.value = ''; loading.value = false; return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]; total.value = FALLBACK.length; dataSource.value = 'mock'; apiError.value = '后端 API 暂未实装, 已加载前端示例数据'; loading.value = false
|
||||||
|
}
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase(); let rs = list.value
|
||||||
|
if (statusTab.value === '1') rs = rs.filter(r => new Date(r.endTime) >= new Date('2026-07-07'))
|
||||||
|
if (kw) rs = rs.filter(r => String(r.promoterName).includes(kw) || String(r.enterpriseName).includes(kw) || String(r.promoterGood).includes(kw))
|
||||||
|
return rs
|
||||||
|
})
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [{ key:'promoterName', label:'推广员姓名', type:'text' },{ key:'enterpriseName', label:'供应商', type:'text' },{ key:'promoterGood', label:'推广商品', type:'text' }]
|
||||||
|
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() { filterKw.value = String(filters.promoterName || filters.enterpriseName || filters.promoterGood || ''); page.value = 1 }
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
async function onDelete(row: PromoterRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认删除推广员「${row.promoterName}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
ElMessage.success('已删除 (前端示例)')
|
||||||
|
}
|
||||||
|
function onEdit(row: PromoterRow) { ElMessage.info(`编辑 ${row.id}`) }
|
||||||
|
function onView(row: PromoterRow) { ElMessage.info(`详情 ${row.id}`) }
|
||||||
|
function onExport() { ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)') }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="推广员管理" sub="商品推广返佣管理" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_DEFAULTS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
<DataTable :columns="columns" :data="filtered"
|
||||||
|
:borderless="true" :loading="loading" :page="page" :limit="limit" :total="total" row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }" @limit-change="(l: number) => { limit = l; page = 1; load() }">
|
||||||
|
<template #radio="{ row }">{{ (row.promoterRadio * 100).toFixed(1) }}%</template>
|
||||||
|
<template #date1="{ row }">{{ row.beginTime }}</template>
|
||||||
|
<template #date2="{ row }">{{ row.endTime }}</template>
|
||||||
|
<template #time="{ row }">{{ row.createTime }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableActions
|
||||||
|
:row="row"
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'primary', onClick: onView },
|
||||||
|
{ label: '编辑', type: 'primary', onClick: onEdit },
|
||||||
|
{ label: '删除', type: 'danger', onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
@ -10,9 +10,14 @@
|
|||||||
* - 批量: 审核/驳回/上架/下架/改分类/删除
|
* - 批量: 审核/驳回/上架/下架/改分类/删除
|
||||||
* - 详情弹窗: 含服务属性 + 服务区域 + 价格日历
|
* - 详情弹窗: 含服务属性 + 服务区域 + 价格日历
|
||||||
*/
|
*/
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface ServiceGood {
|
interface ServiceGood {
|
||||||
id: string
|
id: string
|
||||||
@ -63,6 +68,34 @@ const selection = ref<ServiceGood[]>([])
|
|||||||
|
|
||||||
const filters = reactive({ title: '', enterpriseName: '', checkStatus: '' })
|
const filters = reactive({ title: '', enterpriseName: '', checkStatus: '' })
|
||||||
|
|
||||||
|
// 状态统计 (与 tab 解耦, 跨 tab 一致)
|
||||||
|
const checkStats = reactive({ pending: 0, approved: 0, rejected: 0 })
|
||||||
|
|
||||||
|
// W17.6: 服务商品总数 + SKU 总数 (服务商品 type=2, 全量, 不分 checkStatus)
|
||||||
|
const counts = reactive({ goodCount: 0, skuCount: 0 })
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ type: 'selection' as const, label: '', width: 50 },
|
||||||
|
{ prop: 'serviceGoodId', label: '服务ID', width: 100 },
|
||||||
|
{ prop: 'title', label: '服务名称', minWidth: 320, slot: 'title' },
|
||||||
|
{ prop: 'enterpriseName', label: '供应商', minWidth: 180 },
|
||||||
|
{ prop: 'categoryName', label: '分类', width: 160 },
|
||||||
|
{ prop: 'serviceArea', label: '服务区域', width: 120, align: 'center' as const },
|
||||||
|
{ prop: 'price', label: '价格', width: 160, align: 'right' as const, slot: 'price' },
|
||||||
|
{ prop: 'onShelf', label: '铺货', width: 100, align: 'center' as const, slot: 'onShelf' },
|
||||||
|
{ prop: 'checkStatus', label: '状态', width: 100, align: 'center' as const, slot: 'checkStatus' },
|
||||||
|
{ prop: 'createDate', label: '申请时间', width: 170 },
|
||||||
|
{ label: '操作', width: 140, align: 'center' as const, fixed: 'right' as const, slot: 'action' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const tabItems = computed(() => [
|
||||||
|
// 全部 = 待审核 + 已通过 + 已驳回
|
||||||
|
{ key: '', label: '全部', count: checkStats.pending + checkStats.approved + checkStats.rejected },
|
||||||
|
{ key: '1', label: '待审核', count: checkStats.pending },
|
||||||
|
{ key: '2', label: '已通过', count: checkStats.approved },
|
||||||
|
{ key: '3', label: '已驳回', count: checkStats.rejected }
|
||||||
|
])
|
||||||
|
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
const detailData = ref<ServiceGood | null>(null)
|
const detailData = ref<ServiceGood | null>(null)
|
||||||
const checkDialogVisible = ref(false)
|
const checkDialogVisible = ref(false)
|
||||||
@ -74,26 +107,83 @@ const batchCategoryDialogVisible = ref(false)
|
|||||||
const batchRejectDialogVisible = ref(false)
|
const batchRejectDialogVisible = ref(false)
|
||||||
const batchRejectForm = reactive({ reason: '' })
|
const batchRejectForm = reactive({ reason: '' })
|
||||||
|
|
||||||
|
async function buildSearchParamsForStats() {
|
||||||
|
const params: any = { type: 2 }
|
||||||
|
if (filters.title) params.title = filters.title
|
||||||
|
if (filters.enterpriseName) params.enterpriseName = filters.enterpriseName
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const base = await buildSearchParamsForStats()
|
||||||
|
const [r1, r2, r3] = await Promise.all([
|
||||||
|
httpGet('/supplier/good/pageList', { ...base, checkStatus: 1, page: 1, limit: 1 }),
|
||||||
|
httpGet('/supplier/good/pageList', { ...base, checkStatus: 2, page: 1, limit: 1 }),
|
||||||
|
httpGet('/supplier/good/pageList', { ...base, checkStatus: 3, page: 1, limit: 1 })
|
||||||
|
])
|
||||||
|
checkStats.pending = Number(r1?.total ?? 0)
|
||||||
|
checkStats.approved = Number(r2?.total ?? 0)
|
||||||
|
checkStats.rejected = Number(r3?.total ?? 0)
|
||||||
|
} catch {
|
||||||
|
checkStats.pending = 0
|
||||||
|
checkStats.approved = 0
|
||||||
|
checkStats.rejected = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// W17.6: 全量服务商品数 + 全量 SKU 数 (服务, type=2, 不分 checkStatus / 搜索)
|
||||||
|
async function loadCounts() {
|
||||||
|
try {
|
||||||
|
const r: any = await httpGet('/supplier/good/sku-count', { type: 2 })
|
||||||
|
counts.goodCount = Number(r?.goodCount ?? 0)
|
||||||
|
counts.skuCount = Number(r?.skuCount ?? 0)
|
||||||
|
} catch {
|
||||||
|
counts.goodCount = 0
|
||||||
|
counts.skuCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await new Promise(r => setTimeout(r, 200))
|
// W13.2: /supplier/good/pageList?type=2 是服务商品端点 (与实物商品共用 supplier_good 表, type 区分)
|
||||||
let rows = [...FALLBACK]
|
const data = await httpGet<{ total: number | string; list: any[] }>('/supplier/good/pageList', {
|
||||||
|
page: page.value,
|
||||||
|
limit: limit.value,
|
||||||
|
type: 2
|
||||||
|
})
|
||||||
|
let rows = (data.list || []).map((it: any) => ({
|
||||||
|
id: String(it.id),
|
||||||
|
serviceGoodId: it.supplierGoodId || String(it.id),
|
||||||
|
icon: it.icon || null,
|
||||||
|
title: it.title || '--',
|
||||||
|
enterpriseName: it.enterpriseName || '--',
|
||||||
|
categoryName: [it.firstCategoryName, it.secondCategoryName, it.thirdCategoryName].filter(Boolean).join(' / ') || '--',
|
||||||
|
serviceArea: it.enterpriseAddress || '--',
|
||||||
|
price: it.remark || '--', // 服务的"价格"实际在 remark 字段 (家政类)
|
||||||
|
checkStatus: it.checkStatus ?? 2,
|
||||||
|
onShelf: it.onShelf ?? 1,
|
||||||
|
createDate: it.createDate || '',
|
||||||
|
unit: '--',
|
||||||
|
checkOpinion: it.checkOpinion || '',
|
||||||
|
raw: it
|
||||||
|
}))
|
||||||
if (filters.title) rows = rows.filter(r => r.title.includes(filters.title))
|
if (filters.title) rows = rows.filter(r => r.title.includes(filters.title))
|
||||||
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
|
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
|
||||||
if (filters.checkStatus) rows = rows.filter(r => String(r.checkStatus) === filters.checkStatus)
|
if (filters.checkStatus) rows = rows.filter(r => String(r.checkStatus) === filters.checkStatus)
|
||||||
list.value = rows
|
list.value = rows
|
||||||
total.value = rows.length
|
total.value = Number(data.total) || 0
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.msg || '加载失败'
|
error.value = e?.msg || e?.message || '加载失败'
|
||||||
list.value = FALLBACK
|
list.value = []
|
||||||
total.value = FALLBACK.length
|
total.value = 0
|
||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSearch() { page.value = 1; load() }
|
function onSearch() { page.value = 1; Promise.all([load(), loadStats(), loadCounts()]) }
|
||||||
function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; load() }
|
function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; page.value = 1; Promise.all([load(), loadStats(), loadCounts()]) }
|
||||||
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: ServiceGood[]) { selection.value = rows }
|
function onSelectionChange(rows: ServiceGood[]) { selection.value = rows }
|
||||||
@ -117,42 +207,27 @@ function onDeleteBatch() { if (!selection.value.length) return ElMessage.warning
|
|||||||
function onExport() { ElMessage.success('导出服务商品 (mock)') }
|
function onExport() { ElMessage.success('导出服务商品 (mock)') }
|
||||||
function onAdd() { ElMessage.info('新增服务商品 (mock)') }
|
function onAdd() { ElMessage.info('新增服务商品 (mock)') }
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(() => Promise.all([load(), loadStats(), loadCounts()]))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="服务商品管理" :sub="`共 ${counts.goodCount} 个服务商品,${counts.skuCount} 个SKU`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">服务商品管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个服务商品</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onExport">导出</el-button>
|
<el-button @click="onExport">导出</el-button>
|
||||||
<el-button type="primary" @click="onAdd">新增服务</el-button>
|
<el-button type="primary" @click="onAdd">新增服务</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card rr-search">
|
<SearchBar
|
||||||
<el-form :inline="true" @keyup.enter="onSearch">
|
v-model:filters="filters"
|
||||||
<el-form-item label="服务名称">
|
:fields='[
|
||||||
<el-input v-model="filters.title" placeholder="服务名称" clearable style="width: 200px" />
|
{ key: "title", label: "服务名称", type: "text", placeholder: "服务名称", clearable: true },
|
||||||
</el-form-item>
|
{ key: "enterpriseName", label: "供应商", type: "text", placeholder: "供应商", clearable: true }
|
||||||
<el-form-item label="供应商">
|
]'
|
||||||
<el-input v-model="filters.enterpriseName" placeholder="供应商" clearable style="width: 200px" />
|
@search="onSearch"
|
||||||
</el-form-item>
|
@reset="load"
|
||||||
<el-form-item>
|
/>
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
|
||||||
<el-button @click="onReset">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-tabs-row">
|
<ListTabs v-model="filters.checkStatus" :items="tabItems" @change="onStatusTab" />
|
||||||
<div class="rr-tabs">
|
|
||||||
<div v-for="t in STATUS_TABS" :key="t.value" class="rr-tab" :class="{ active: filters.checkStatus === t.value }" @click="onStatusTab(t.value)">{{ t.label }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-batch-bar" v-if="selection.length > 0">
|
<div class="rr-batch-bar" v-if="selection.length > 0">
|
||||||
<span class="rr-batch-tip">已选 {{ selection.length }} 项</span>
|
<span class="rr-batch-tip">已选 {{ selection.length }} 项</span>
|
||||||
@ -163,51 +238,52 @@ onMounted(load)
|
|||||||
<el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button>
|
<el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rr-card">
|
<DataTable
|
||||||
<el-table :data="list" border style="width: 100%" v-loading="loading" :empty-text="error || '暂无数据'" @selection-change="onSelectionChange">
|
:columns="columns"
|
||||||
<el-table-column type="selection" width="50" align="center" />
|
:data="list"
|
||||||
<el-table-column prop="serviceGoodId" label="服务ID" width="140" align="center" />
|
:loading="loading"
|
||||||
<el-table-column label="服务名称" min-width="240">
|
:total="total"
|
||||||
<template #default="{ row }">
|
:page="page"
|
||||||
|
:limit="limit"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:show-pagination="true"
|
||||||
|
:error="error"
|
||||||
|
empty-text="暂无数据"
|
||||||
|
:borderless="true"
|
||||||
|
row-key="id"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
@limit-change="onLimitChange"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
|
>
|
||||||
|
<template #title="{ row }">
|
||||||
<div class="rr-good-cell">
|
<div class="rr-good-cell">
|
||||||
<div v-if="row.icon" class="rr-good-icon"><el-image :src="row.icon" :preview-src-list="[row.icon]" fit="cover" style="width: 40px; height: 40px; border-radius: 4px" /></div>
|
<div v-if="row.icon" class="rr-good-icon"><el-image :src="row.icon" :preview-src-list="[row.icon]" fit="cover" style="width: 40px; height: 40px; border-radius: 4px" /></div>
|
||||||
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div>
|
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div>
|
||||||
<span>{{ row.title }}</span>
|
<span>{{ row.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
<template #price="{ row }">
|
||||||
<el-table-column prop="enterpriseName" label="供应商" min-width="180" show-overflow-tooltip />
|
<span class="rr-price">¥ {{ Number(row.price || 0).toLocaleString() }} / {{ row.unit }}</span>
|
||||||
<el-table-column prop="categoryName" label="分类" width="160" show-overflow-tooltip />
|
</template>
|
||||||
<el-table-column prop="serviceArea" label="服务区域" width="120" align="center" />
|
<template #onShelf="{ row }">
|
||||||
<el-table-column label="价格" width="140" align="right">
|
|
||||||
<template #default="{ row }">¥ {{ row.price.toLocaleString() }} / {{ row.unit }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="铺货" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</el-tag>
|
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
<template #checkStatus="{ row }">
|
||||||
<el-table-column prop="checkOpinion" label="审核意见" min-width="160" show-overflow-tooltip />
|
|
||||||
<el-table-column label="状态" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="(CHECK_STATUS_MAP[row.checkStatus]?.type as any)" size="small" effect="light">{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</el-tag>
|
<el-tag :type="(CHECK_STATUS_MAP[row.checkStatus]?.type as any)" size="small" effect="light">{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
<template #action="{ row }">
|
||||||
<el-table-column label="申请时间" prop="createDate" width="170" />
|
<TableActions
|
||||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
:row="row"
|
||||||
<template #default="{ row }">
|
:actions="[
|
||||||
<el-button text type="primary" size="small" @click="onShowDetail(row)">详情</el-button>
|
{ label: '详情', type: 'primary', onClick: onShowDetail },
|
||||||
<el-button v-if="row.checkStatus === 1" text type="primary" size="small" @click="onShowCheck(row)">审核</el-button>
|
{ label: '审核', type: 'primary', when: (r: any) => r.checkStatus === 1, onClick: onShowCheck },
|
||||||
<el-button text :type="row.onShelf === 1 ? 'warning' : 'success'" size="small" @click="onOnShelf(row)">{{ row.onShelf === 1 ? '下架' : '上架' }}</el-button>
|
{ label: (r: any) => r.onShelf === 1 ? '下架' : '上架', type: (r: any) => r.onShelf === 1 ? 'warning' : 'success', onClick: onOnShelf },
|
||||||
<el-button text type="primary" size="small" @click="onShowCategory(row)">改分类</el-button>
|
{ label: '改分类', type: 'primary', onClick: onShowCategory },
|
||||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
{ label: '删除', type: 'danger', divided: true, onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</DataTable>
|
||||||
</el-table>
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-dialog v-model="detailVisible" title="服务详情" width="700px">
|
<el-dialog v-model="detailVisible" title="服务详情" width="700px">
|
||||||
<el-descriptions v-if="detailData" :column="2" border>
|
<el-descriptions v-if="detailData" :column="2" border>
|
||||||
@ -284,19 +360,10 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1500px; }
|
.rr-page { max-width: 1500px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
||||||
.rr-search { padding: 16px 20px; }
|
|
||||||
.rr-tabs-row { margin-bottom: 16px; }
|
|
||||||
.rr-tabs { display: flex; gap: 4px; padding: 4px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); width: fit-content; }
|
|
||||||
.rr-tab { padding: 6px 16px; border-radius: var(--radius); font-size: 13px; color: var(--ink-2); cursor: pointer; }
|
|
||||||
.rr-tab.active { color: white; background: var(--primary); font-weight: 500; }
|
|
||||||
.rr-batch-bar { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: var(--bg-soft); border-radius: var(--radius); margin-bottom: 12px; }
|
.rr-batch-bar { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: var(--bg-soft); border-radius: var(--radius); margin-bottom: 12px; }
|
||||||
.rr-batch-tip { font-size: 13px; color: var(--ink-2); margin-right: 8px; }
|
.rr-batch-tip { font-size: 13px; color: var(--ink-2); margin-right: 8px; }
|
||||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
|
||||||
.rr-good-cell { display: flex; align-items: center; gap: 10px; }
|
.rr-good-cell { display: flex; align-items: center; gap: 10px; }
|
||||||
.rr-good-icon-placeholder { width: 40px; height: 40px; border-radius: 4px; background: var(--bg-soft); display: flex; align-items: center; justify-content: center; color: var(--ink-3); font-weight: 600; flex-shrink: 0; }
|
.rr-good-icon-placeholder { width: 40px; height: 40px; border-radius: 4px; background: var(--bg-soft); display: flex; align-items: center; justify-content: center; color: var(--ink-3); font-weight: 600; flex-shrink: 0; }
|
||||||
|
.rr-price { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 13px; font-weight: 500; color: var(--primary); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 部门管理 - W4.14
|
* F006 部门管理 - W4.14
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/dept/list (返树结构)
|
* 后端: GET /sys/dept/list (返树结构)
|
||||||
* 字段: id/pid/name/sort/createDate/parentName
|
* 字段: id/pid/name/sort/createDate/parentName
|
||||||
*
|
*
|
||||||
@ -11,6 +13,8 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Dept {
|
interface Dept {
|
||||||
id: string
|
id: string
|
||||||
@ -144,15 +148,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-dept">
|
<div class="rr-sys-dept">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="部门管理" :sub="``">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">部门管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ list.length }} 个部门(树形)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增部门</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增部门</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -169,6 +167,7 @@ onMounted(load)
|
|||||||
:page="1"
|
:page="1"
|
||||||
:limit="999"
|
:limit="999"
|
||||||
:show-pagination="false"
|
:show-pagination="false"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无部门"
|
empty-text="暂无部门"
|
||||||
borderless
|
borderless
|
||||||
@ -222,6 +221,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-dept { padding: 16px; }
|
.rr-sys-dept { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 字典数据 - W4.18
|
* F006 字典数据 - W4.18
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/dict/data/page
|
* 后端: GET /sys/dict/data/page
|
||||||
* 字段: id/dictTypeId/dictLabel/dictValue/dictSort/remark/createDate
|
* 字段: id/dictTypeId/dictLabel/dictValue/dictSort/remark/createDate
|
||||||
*
|
*
|
||||||
@ -11,6 +13,8 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface DictData {
|
interface DictData {
|
||||||
id: string
|
id: string
|
||||||
@ -138,21 +142,19 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-dict-data">
|
<div class="rr-sys-dict-data">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="字典数据" :sub="`共 ${total} 条`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">字典数据</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 条字典项</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增字典项</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增字典项</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.dictLabel" placeholder="字典标签搜索" clearable class="rr-label-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
:fields='[
|
||||||
</div>
|
{ key: "dictLabel", label: "字典标签", type: "text", placeholder: "字典标签" }
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -170,7 +172,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无字典数据"
|
empty-text="暂无字典数据"
|
||||||
borderless
|
borderless
|
||||||
@ -222,6 +224,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-dict-data { padding: 16px; }
|
.rr-sys-dict-data { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 字典类型 - W4.18
|
* F006 字典类型 - W4.18
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/dict/type/page
|
* 后端: GET /sys/dict/type/page
|
||||||
* 字段: id/dictType/dictName/remark/createDate
|
* 字段: id/dictType/dictName/remark/createDate
|
||||||
*
|
*
|
||||||
@ -11,6 +13,8 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface DictType {
|
interface DictType {
|
||||||
id: string
|
id: string
|
||||||
@ -128,21 +132,21 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-dict-type">
|
<div class="rr-sys-dict-type">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="字典类型" :sub="`共 ${total} 个字典`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">字典类型</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个字典类型</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增字典类型</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增字典类型</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.dictType" placeholder="字典类型搜索" clearable class="rr-type-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
:fields='[
|
||||||
</div>
|
{ key: "dictName", label: "字典名", type: "text", placeholder: "字典名" },
|
||||||
|
{ key: "dictType", label: "字典类型", type: "text", placeholder: "字典类型" }
|
||||||
|
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -158,7 +162,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无字典类型"
|
empty-text="暂无字典类型"
|
||||||
borderless
|
borderless
|
||||||
@ -204,6 +208,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-dict-type { padding: 16px; }
|
.rr-sys-dict-type { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 文件管理 - W4.16
|
* F006 文件管理 - W4.16
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/file/page
|
* 后端: GET /sys/file/page
|
||||||
* 字段: id/url/creator/createDate
|
* 字段: id/url/creator/createDate
|
||||||
*/
|
*/
|
||||||
@ -9,6 +11,8 @@ import { onMounted, ref } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet, httpDelete } from '@/utils/request'
|
import { httpGet, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface FileItem {
|
interface FileItem {
|
||||||
id: string
|
id: string
|
||||||
@ -85,15 +89,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-file">
|
<div class="rr-sys-file">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="文件管理" :sub="`共 ${total} 个文件`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">文件管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个文件</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="load" class="rr-btn-secondary">刷新</el-button>
|
<el-button @click="load" class="rr-btn-secondary">刷新</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -110,6 +108,7 @@ onMounted(load)
|
|||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无文件"
|
empty-text="暂无文件"
|
||||||
borderless
|
borderless
|
||||||
@ -133,6 +132,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-file { padding: 16px; }
|
.rr-sys-file { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -2,12 +2,16 @@
|
|||||||
/**
|
/**
|
||||||
* F006 错误日志 - W4.16
|
* F006 错误日志 - W4.16
|
||||||
*
|
*
|
||||||
|
* W17.5: 套 PageHeader/SearchBar 标准化模板 (limit 10 + :page-sizes)
|
||||||
|
*
|
||||||
* 后端: GET /sys/log/error/page
|
* 后端: GET /sys/log/error/page
|
||||||
* 字段: id/module/requestUri/requestMethod/requestParams/userAgent/ip/errorInfo/createDate
|
* 字段: id/module/requestUri/requestMethod/requestParams/userAgent/ip/errorInfo/createDate
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } 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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
id: string
|
id: string
|
||||||
@ -27,7 +31,7 @@ const total = ref(0)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const limit = ref(20)
|
const limit = ref(10)
|
||||||
const filters = ref({ module: '' })
|
const filters = ref({ module: '' })
|
||||||
|
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
@ -91,20 +95,16 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-log-error">
|
<div class="rr-sys-log-error">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="错误日志" :sub="`共 ${total} 条错误记录`" />
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">错误日志</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 条错误记录</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.module" placeholder="模块名" clearable class="rr-module-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
:fields='[
|
||||||
</div>
|
{ key: "module", label: "模块名", type: "text", placeholder: "模块名" }
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -123,6 +123,7 @@ onMounted(load)
|
|||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无错误日志"
|
empty-text="暂无错误日志"
|
||||||
borderless
|
borderless
|
||||||
@ -193,13 +194,6 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-log-error { padding: 16px; }
|
.rr-sys-log-error { padding: 16px; }
|
||||||
.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-title { margin: 0; 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-module-input { width: 180px; }
|
|
||||||
.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; }
|
||||||
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
||||||
.rr-mono-sm { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 11px; color: var(--el-text-color-regular); }
|
.rr-mono-sm { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 11px; color: var(--el-text-color-regular); }
|
||||||
@ -217,10 +211,10 @@ onMounted(load)
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||||
}
|
}
|
||||||
.rr-method-get { background: #DBEAFE; color: #2563EB; }
|
.rr-method-get { background: var(--brand-soft); color: var(--brand); }
|
||||||
.rr-method-post { background: #D1FAE5; color: #059669; }
|
.rr-method-post { background: var(--green-soft); color: var(--green); }
|
||||||
.rr-method-put { background: #FEF3C7; color: #D97706; }
|
.rr-method-put { background: var(--amber-soft); color: var(--amber); }
|
||||||
.rr-method-delete { background: #FEE2E2; color: #DC2626; }
|
.rr-method-delete { background: var(--red-soft); color: var(--red); }
|
||||||
.rr-error-detail { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 20px; }
|
.rr-error-detail { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 20px; }
|
||||||
.rr-detail-row { display: flex; flex-direction: column; gap: 4px; }
|
.rr-detail-row { display: flex; flex-direction: column; gap: 4px; }
|
||||||
.rr-detail-full { grid-column: 1 / -1; }
|
.rr-detail-full { grid-column: 1 / -1; }
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 登录日志 - W4.14
|
* F006 登录日志 - W4.14
|
||||||
*
|
*
|
||||||
|
* W17.5: 套 PageHeader/SearchBar 标准化模板 (limit 10 + :page-sizes)
|
||||||
|
*
|
||||||
* 后端: GET /sys/log/login/page
|
* 后端: GET /sys/log/login/page
|
||||||
* 字段: id/operation/status/userAgent/ip/creatorName/createDate
|
* 字段: id/operation/status/userAgent/ip/creatorName/createDate
|
||||||
*
|
*
|
||||||
@ -10,6 +12,8 @@
|
|||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } 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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
id: string
|
id: string
|
||||||
@ -38,7 +42,7 @@ const total = ref(0)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const limit = ref(20)
|
const limit = ref(10)
|
||||||
|
|
||||||
const filters = ref({ creatorName: '', ip: '' })
|
const filters = ref({ creatorName: '', ip: '' })
|
||||||
|
|
||||||
@ -92,35 +96,17 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-log-login">
|
<div class="rr-sys-log-login">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="登录日志" :sub="`共 ${total} 条登录记录`" />
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">登录日志</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 条登录记录</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input
|
v-model:filters="filters"
|
||||||
v-model="filters.creatorName"
|
:fields='[
|
||||||
placeholder="用户名搜索"
|
{ key: "creatorName", label: "用户名", type: "text", placeholder: "用户名搜索" },
|
||||||
clearable
|
{ key: "ip", label: "IP", type: "text", placeholder: "IP 搜索" }
|
||||||
class="rr-name-input"
|
]'
|
||||||
size="small"
|
@search="onSearch"
|
||||||
@keyup.enter="onSearch"
|
@reset="onReset"
|
||||||
/>
|
/>
|
||||||
<el-input
|
|
||||||
v-model="filters.ip"
|
|
||||||
placeholder="IP 搜索"
|
|
||||||
clearable
|
|
||||||
class="rr-ip-input"
|
|
||||||
size="small"
|
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -138,6 +124,7 @@ onMounted(load)
|
|||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无登录日志"
|
empty-text="暂无登录日志"
|
||||||
borderless
|
borderless
|
||||||
@ -167,14 +154,6 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-log-login { padding: 16px; }
|
.rr-sys-log-login { padding: 16px; }
|
||||||
.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-title { margin: 0; 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-name-input { width: 200px; }
|
|
||||||
.rr-ip-input { width: 200px; }
|
|
||||||
.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; }
|
||||||
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
||||||
</style>
|
</style>
|
||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 操作日志 - W4.14
|
* F006 操作日志 - W4.14
|
||||||
*
|
*
|
||||||
|
* W17.5: 套 PageHeader/SearchBar 标准化模板 (limit 10 + :page-sizes)
|
||||||
|
*
|
||||||
* 后端: GET /sys/log/operation/page
|
* 后端: GET /sys/log/operation/page
|
||||||
* 字段: id/module/operation/requestUri/requestMethod/requestParams/requestTime/userAgent/ip/status/creatorName/createDate
|
* 字段: id/module/operation/requestUri/requestMethod/requestParams/requestTime/userAgent/ip/status/creatorName/createDate
|
||||||
*
|
*
|
||||||
@ -10,6 +12,8 @@
|
|||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } 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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
id: string
|
id: string
|
||||||
@ -37,7 +41,7 @@ const total = ref(0)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const limit = ref(20)
|
const limit = ref(10)
|
||||||
|
|
||||||
const filters = ref({ creatorName: '', module: '' })
|
const filters = ref({ creatorName: '', module: '' })
|
||||||
|
|
||||||
@ -103,35 +107,17 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-log-operation">
|
<div class="rr-sys-log-operation">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="操作日志" :sub="`共 ${total} 条操作记录`" />
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">操作日志</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 条操作记录</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input
|
v-model:filters="filters"
|
||||||
v-model="filters.creatorName"
|
:fields='[
|
||||||
placeholder="操作用户"
|
{ key: "creatorName", label: "操作用户", type: "text", placeholder: "操作用户" },
|
||||||
clearable
|
{ key: "module", label: "模块名", type: "text", placeholder: "模块名" }
|
||||||
class="rr-name-input"
|
]'
|
||||||
size="small"
|
@search="onSearch"
|
||||||
@keyup.enter="onSearch"
|
@reset="onReset"
|
||||||
/>
|
/>
|
||||||
<el-input
|
|
||||||
v-model="filters.module"
|
|
||||||
placeholder="模块名"
|
|
||||||
clearable
|
|
||||||
class="rr-module-input"
|
|
||||||
size="small"
|
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -151,6 +137,7 @@ onMounted(load)
|
|||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无操作日志"
|
empty-text="暂无操作日志"
|
||||||
borderless
|
borderless
|
||||||
@ -191,14 +178,6 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-log-operation { padding: 16px; }
|
.rr-sys-log-operation { padding: 16px; }
|
||||||
.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-title { margin: 0; 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-name-input { width: 180px; }
|
|
||||||
.rr-module-input { width: 180px; }
|
|
||||||
.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; }
|
||||||
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
||||||
.rr-mono-sm { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 11px; color: var(--el-text-color-regular); }
|
.rr-mono-sm { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 11px; color: var(--el-text-color-regular); }
|
||||||
@ -210,8 +189,8 @@ onMounted(load)
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||||
}
|
}
|
||||||
.rr-method-get { background: #DBEAFE; color: #2563EB; }
|
.rr-method-get { background: var(--brand-soft); color: var(--brand); }
|
||||||
.rr-method-post { background: #D1FAE5; color: #059669; }
|
.rr-method-post { background: var(--green-soft); color: var(--green); }
|
||||||
.rr-method-put { background: #FEF3C7; color: #D97706; }
|
.rr-method-put { background: var(--amber-soft); color: var(--amber); }
|
||||||
.rr-method-delete { background: #FEE2E2; color: #DC2626; }
|
.rr-method-delete { background: var(--red-soft); color: var(--red); }
|
||||||
</style>
|
</style>
|
||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 菜单管理 - W4.14
|
* F006 菜单管理 - W4.14
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader (树形保留 el-table)
|
||||||
|
*
|
||||||
* 后端: GET /sys/menu/list (返树结构)
|
* 后端: GET /sys/menu/list (返树结构)
|
||||||
* 字段: id/pid/name/url/type/permissions/icon/sort/createDate
|
* 字段: id/pid/name/url/type/permissions/icon/sort/createDate
|
||||||
*
|
*
|
||||||
@ -11,6 +13,7 @@ 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 { httpGet, httpPost, 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'
|
||||||
|
|
||||||
interface Menu {
|
interface Menu {
|
||||||
id: string
|
id: string
|
||||||
@ -168,15 +171,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-menu">
|
<div class="rr-sys-menu">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="菜单管理" :sub="`共 ${list.length} 个菜单`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">菜单管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ list.length }} 个菜单(树形)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增菜单</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增菜单</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -196,6 +193,7 @@ onMounted(load)
|
|||||||
:page="1"
|
:page="1"
|
||||||
:limit="999"
|
:limit="999"
|
||||||
:show-pagination="false"
|
:show-pagination="false"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无菜单"
|
empty-text="暂无菜单"
|
||||||
borderless
|
borderless
|
||||||
@ -272,6 +270,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-menu { padding: 16px; }
|
.rr-sys-menu { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 通知公告 - W4.18
|
* F006 通知公告 - W4.18
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/notice/page
|
* 后端: GET /sys/notice/page
|
||||||
* 字段: id/title/content/type/status/publisherName/createDate
|
* 字段: id/title/content/type/status/publisherName/createDate
|
||||||
*
|
*
|
||||||
@ -11,6 +13,8 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Notice {
|
interface Notice {
|
||||||
id: string
|
id: string
|
||||||
@ -154,21 +158,19 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-notice">
|
<div class="rr-sys-notice">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="通知公告" :sub="`共 ${total} 条`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">通知公告</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 条公告</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增公告</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增公告</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.title" placeholder="标题搜索" clearable class="rr-title-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
:fields='[
|
||||||
</div>
|
{ key: "title", label: "标题", type: "text", placeholder: "标题" }
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -186,7 +188,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无公告"
|
empty-text="暂无公告"
|
||||||
borderless
|
borderless
|
||||||
@ -253,6 +255,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-notice { padding: 16px; }
|
.rr-sys-notice { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import { onMounted, ref, reactive, computed } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet, httpPost, httpPut, httpDelete } from '@/utils/request'
|
import { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
interface SysParams {
|
interface SysParams {
|
||||||
@ -193,19 +195,10 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-params">
|
<div class="rr-sys-params">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="参数管理" :sub="hasParamsPerm ? `共 ${total} 个参数` : '当前角色无参数管理权限'">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">参数管理</h2>
|
|
||||||
<p class="rr-page-sub">
|
|
||||||
<template v-if="hasParamsPerm">共 {{ total }} 个参数</template>
|
|
||||||
<template v-else>当前角色无参数管理权限</template>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
<el-button type="primary" class="rr-btn-primary" :disabled="!hasParamsPerm" @click="createHandle">+ 新增参数</el-button>
|
<el-button type="primary" class="rr-btn-primary" :disabled="!hasParamsPerm" @click="createHandle">+ 新增参数</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 无权限提示卡 -->
|
<!-- 无权限提示卡 -->
|
||||||
<el-alert
|
<el-alert
|
||||||
@ -222,17 +215,14 @@ onMounted(() => {
|
|||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input
|
v-model:filters="filters"
|
||||||
v-model="filters.paramCode"
|
:fields='[
|
||||||
placeholder="参数编码搜索"
|
{ key: "paramKey", label: "参数键", type: "text", placeholder: "参数键" }
|
||||||
clearable
|
]'
|
||||||
class="rr-code-input"
|
@search="onSearch"
|
||||||
size="small"
|
@reset="onReset"
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
/>
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -244,12 +234,12 @@ onMounted(() => {
|
|||||||
{ prop: 'updateDate', label: '更新时间', width: 170, slot: 'updateDate' },
|
{ prop: 'updateDate', label: '更新时间', width: 170, slot: 'updateDate' },
|
||||||
{ label: '操作', width: 180, slot: 'action' }
|
{ label: '操作', width: 180, slot: 'action' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list" :borderless="true"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="loadError"
|
:error="loadError"
|
||||||
empty-text="暂无参数"
|
empty-text="暂无参数"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 岗位管理 - W4.16
|
* F006 岗位管理 - W4.16
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/post/page
|
* 后端: GET /sys/post/page
|
||||||
* 字段: id/postCode/postName/sort/status/createDate
|
* 字段: id/postCode/postName/sort/status/createDate
|
||||||
*/
|
*/
|
||||||
@ -9,6 +11,8 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
id: string
|
id: string
|
||||||
@ -126,22 +130,21 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-post">
|
<div class="rr-sys-post">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="岗位管理" :sub="`共 ${total} 个岗位`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">岗位管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个岗位</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增岗位</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增岗位</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.postCode" placeholder="岗位编码" clearable class="rr-code-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-input v-model="filters.postName" placeholder="岗位名称" clearable class="rr-name-input" size="small" @keyup.enter="onSearch" />
|
:fields='[
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
{ key: "postCode", label: "岗位编码", type: "text", placeholder: "岗位编码" },
|
||||||
</div>
|
{ key: "postName", label: "岗位名称", type: "text", placeholder: "岗位名称" }
|
||||||
|
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -158,7 +161,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无岗位"
|
empty-text="暂无岗位"
|
||||||
borderless
|
borderless
|
||||||
@ -213,6 +216,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-post { padding: 16px; }
|
.rr-sys-post { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 商品管理 - W4.18
|
* F006 商品管理 - W4.18
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/product/page
|
* 后端: GET /sys/product/page
|
||||||
* 字段: id/name/categoryId/originPrice/price/stock/status/createDate
|
* 字段: id/name/categoryId/originPrice/price/stock/status/createDate
|
||||||
*
|
*
|
||||||
@ -11,6 +13,8 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
id: string
|
id: string
|
||||||
@ -152,21 +156,19 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-product">
|
<div class="rr-sys-product">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="商品管理" :sub="`共 ${total} 个商品`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">商品管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个商品</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增商品</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增商品</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.name" placeholder="商品名搜索" clearable class="rr-name-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
:fields='[
|
||||||
</div>
|
{ key: "name", label: "商品名称", type: "text", placeholder: "商品名称" }
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -185,7 +187,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无商品"
|
empty-text="暂无商品"
|
||||||
borderless
|
borderless
|
||||||
@ -247,6 +249,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-product { padding: 16px; }
|
.rr-sys-product { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 角色组管理 - W4.16
|
* F006 角色组管理 - W4.16
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/roleGroup/page
|
* 后端: GET /sys/roleGroup/page
|
||||||
* 字段: id/operationId/name/createDate
|
* 字段: id/operationId/name/createDate
|
||||||
*/
|
*/
|
||||||
@ -9,6 +11,8 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface RoleGroup {
|
interface RoleGroup {
|
||||||
id: string
|
id: string
|
||||||
@ -111,21 +115,19 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-role-group">
|
<div class="rr-sys-role-group">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="角色组管理" :sub="`共 ${total} 个角色组`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">角色组管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个角色组</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增角色组</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增角色组</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.name" placeholder="角色组名" clearable class="rr-name-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
:fields='[
|
||||||
</div>
|
{ key: "name", label: "角色组名", type: "text", placeholder: "角色组名" }
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -140,7 +142,7 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无角色组"
|
empty-text="暂无角色组"
|
||||||
borderless
|
borderless
|
||||||
@ -182,6 +184,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-role-group { padding: 16px; }
|
.rr-sys-role-group { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -20,6 +20,8 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface SysRole {
|
interface SysRole {
|
||||||
id: string
|
id: string
|
||||||
@ -178,28 +180,19 @@ onMounted(() => { load() })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-role">
|
<div class="rr-sys-role">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="角色管理" :sub="`共 ${total} 个角色`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">角色管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个角色</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增角色</el-button>
|
<el-button type="primary" class="rr-btn-primary" @click="createHandle">+ 新增角色</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input
|
v-model:filters="filters"
|
||||||
v-model="filters.name"
|
:fields='[
|
||||||
placeholder="角色名搜索"
|
{ key: "roleName", label: "角色名", type: "text", placeholder: "角色名" }
|
||||||
clearable
|
]'
|
||||||
class="rr-name-input"
|
@search="onSearch"
|
||||||
size="small"
|
@reset="onReset"
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
/>
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -211,12 +204,12 @@ onMounted(() => { load() })
|
|||||||
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
||||||
{ label: '操作', width: 180, slot: 'action' }
|
{ label: '操作', width: 180, slot: 'action' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list" :borderless="true"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="loadError"
|
:error="loadError"
|
||||||
empty-text="暂无角色"
|
empty-text="暂无角色"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* F006 定时任务 - W4.16
|
* F006 定时任务 - W4.16
|
||||||
*
|
*
|
||||||
|
* W11 A8: 套 PageHeader/SearchBar 标准化模板
|
||||||
|
*
|
||||||
* 后端: GET /sys/schedule/page
|
* 后端: GET /sys/schedule/page
|
||||||
* 字段: id/beanName/methodName/params/cron/expression/status/remark/createDate
|
* 字段: id/beanName/methodName/params/cron/expression/status/remark/createDate
|
||||||
*/
|
*/
|
||||||
@ -9,6 +11,9 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface Schedule {
|
interface Schedule {
|
||||||
id: string
|
id: string
|
||||||
@ -121,20 +126,18 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-schedule">
|
<div class="rr-sys-schedule">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="定时任务" :sub="`共 ${total} 个任务`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">定时任务</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个定时任务</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.beanName" placeholder="Bean 名搜索" clearable class="rr-bean-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
:fields='[
|
||||||
</div>
|
{ key: "beanName", label: "Bean 名", type: "text", placeholder: "Bean 名" }
|
||||||
|
]'
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -146,14 +149,14 @@ onMounted(load)
|
|||||||
{ prop: 'remark', label: '备注', minWidth: 200 },
|
{ prop: 'remark', label: '备注', minWidth: 200 },
|
||||||
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
||||||
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
||||||
{ label: '操作', width: 280, slot: 'action' }
|
{ label: '操作', width: 140, slot: 'action' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="error"
|
:error="error"
|
||||||
empty-text="暂无定时任务"
|
empty-text="暂无定时任务"
|
||||||
borderless
|
borderless
|
||||||
@ -168,10 +171,15 @@ onMounted(load)
|
|||||||
</template>
|
</template>
|
||||||
<template #createDate="{ row }"><span class="rr-time">{{ fmtDate(row.createDate) }}</span></template>
|
<template #createDate="{ row }"><span class="rr-time">{{ fmtDate(row.createDate) }}</span></template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<el-button text type="primary" size="small" @click="runHandle(row)">执行</el-button>
|
<TableActions
|
||||||
<el-button v-if="row.status === 1" text type="warning" size="small" @click="pauseHandle(row)">暂停</el-button>
|
:row="row"
|
||||||
<el-button v-else text type="primary" size="small" @click="resumeHandle(row)">恢复</el-button>
|
:actions="[
|
||||||
<el-button text type="primary" size="small" @click="deleteHandle(row)">删除</el-button>
|
{ label: '执行', type: 'primary', onClick: runHandle },
|
||||||
|
{ label: '暂停', type: 'warning', when: (r: any) => r.status === 1, onClick: pauseHandle },
|
||||||
|
{ label: '恢复', type: 'primary', when: (r: any) => r.status !== 1, onClick: resumeHandle },
|
||||||
|
{ label: '删除', type: 'danger', onClick: deleteHandle }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
@ -180,6 +188,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-schedule { padding: 16px; }
|
.rr-sys-schedule { padding: 16px; }
|
||||||
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
||||||
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
|
.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: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||||
|
|||||||
@ -26,6 +26,9 @@ 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 { httpGet, httpPost, 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface SysUser {
|
interface SysUser {
|
||||||
id: string
|
id: string
|
||||||
@ -227,39 +230,18 @@ onMounted(() => { load() })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-user">
|
<div class="rr-sys-user">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="后台用户" :sub="`共 ${total} 个用户`">
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">用户管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个用户(含 {{ USER_TYPE_MAP[99] ? USER_TYPE_MAP[99].label : 'AI公司' }} 账号)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input
|
v-model:filters="filters"
|
||||||
v-model="filters.username"
|
:fields='[
|
||||||
placeholder="用户名搜索"
|
{ key: "username", label: "用户名", type: "text", placeholder: "用户名" }
|
||||||
clearable
|
]'
|
||||||
class="rr-username-input"
|
@search="onSearch"
|
||||||
size="small"
|
@reset="onReset"
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
/>
|
||||||
<el-select
|
|
||||||
v-model="filters.userType"
|
|
||||||
placeholder="用户类型"
|
|
||||||
class="rr-type-select"
|
|
||||||
clearable
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<el-option value="1" label="试用期" />
|
|
||||||
<el-option value="2" label="正式员工" />
|
|
||||||
<el-option value="3" label="外包员工" />
|
|
||||||
<el-option value="99" label="AI 公司" />
|
|
||||||
</el-select>
|
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -271,14 +253,14 @@ onMounted(() => { load() })
|
|||||||
{ prop: 'userType', label: '类型', width: 100, slot: 'userType' },
|
{ prop: 'userType', label: '类型', width: 100, slot: 'userType' },
|
||||||
{ prop: 'status', label: '状态', width: 80, slot: 'status' },
|
{ prop: 'status', label: '状态', width: 80, slot: 'status' },
|
||||||
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' },
|
||||||
{ label: '操作', width: 260, slot: 'action' }
|
{ label: '操作', width: 140, slot: 'action' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list" :borderless="true"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
||||||
:error="loadError"
|
:error="loadError"
|
||||||
empty-text="暂无用户"
|
empty-text="暂无用户"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@ -312,32 +294,14 @@ onMounted(() => { load() })
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<el-button
|
<TableActions
|
||||||
v-if="row.status === 1"
|
:row="row"
|
||||||
text
|
:actions="[
|
||||||
type="warning"
|
{ label: (r: any) => r.status === 1 ? '停用' : '启用', type: (r: any) => r.status === 1 ? 'warning' : 'success', onClick: toggleStatus },
|
||||||
size="small"
|
{ label: '重置密码', type: 'primary', onClick: resetPwdHandle },
|
||||||
@click="toggleStatus(row)"
|
{ label: '删除', type: 'danger', onClick: deleteHandle }
|
||||||
>停用</el-button>
|
]"
|
||||||
<el-button
|
/>
|
||||||
v-else
|
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="toggleStatus(row)"
|
|
||||||
>启用</el-button>
|
|
||||||
<el-button
|
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="resetPwdHandle(row)"
|
|
||||||
>重置密码</el-button>
|
|
||||||
<el-button
|
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="deleteHandle(row)"
|
|
||||||
>删除</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
49
src/modules/sys/noticeuser/NoticeUserListView.vue
Normal file
49
src/modules/sys/noticeuser/NoticeUserListView.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 用户通知 - P0 真 view
|
||||||
|
* 走 /sys/notice/userPage (2026-08-07 探测: 200, total=0, 接口正常暂无数据)
|
||||||
|
* 字段(推测): id/title/content/type/readFlag/receiver/createDate
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface NoticeUser { id: string; title: string; content: string; type: number; readFlag: number; receiver: string; createDate: string }
|
||||||
|
const READ_MAP: Record<number, { label: string; type: string }> = { 0: { label: '未读', type: 'warning' }, 1: { label: '已读', type: 'info' } }
|
||||||
|
const list = ref<NoticeUser[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
|
{ prop: 'title', label: '标题', minWidth: 200 },
|
||||||
|
{ prop: 'content', label: '内容', minWidth: 250, formatter: (r: NoticeUser) => r.content ? r.content.slice(0, 60) + '...' : '--' },
|
||||||
|
{ prop: 'type', label: '类型', width: 100 },
|
||||||
|
{ prop: 'readFlag', label: '阅读状态', width: 100, slot: 'read' },
|
||||||
|
{ prop: 'receiver', label: '接收人', width: 120 },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 180 }
|
||||||
|
]
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try { const r: any = await httpGet('/sys/notice/userPage', { page: query.page, limit: query.limit }); list.value = r?.list || []; total.value = Number(r?.total || 0) }
|
||||||
|
catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="用户通知" :sub="`共 ${total} 条`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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" @limit-change="onLimitChange">
|
||||||
|
<template #read="{ row }"><el-tag :type="(READ_MAP[row.readFlag]?.type as any) || 'info'">{{ READ_MAP[row.readFlag]?.label || row.readFlag }}</el-tag></template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
80
src/modules/sys/operation/OperationListView.vue
Normal file
80
src/modules/sys/operation/OperationListView.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 运营中心 - P1 真 view
|
||||||
|
*
|
||||||
|
* 走 /sys/operation/pageList (type 区分: 1全国 2省级 3县级 4协会)
|
||||||
|
* 真实接口 (2026-08-07 探测: 200, total=1)
|
||||||
|
* 字段: id/name/type/provinceName/cityName/areaName/memberNo/rebateRadio/status/createDate
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface Operation {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: number
|
||||||
|
provinceName: string | null
|
||||||
|
cityName: string | null
|
||||||
|
areaName: string | null
|
||||||
|
memberNo: string | null
|
||||||
|
rebateRadio: number | null
|
||||||
|
status: number
|
||||||
|
createDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ type?: number }>()
|
||||||
|
|
||||||
|
const TITLES: Record<number, string> = { 1: '全国运营中心', 2: '省级运营中心', 3: '县级运营中心', 4: '行业协会运营' }
|
||||||
|
const STATUS_MAP: Record<number, { label: string; type: string }> = { 0: { label: '停用', type: 'danger' }, 1: { label: '启用', type: 'success' } }
|
||||||
|
|
||||||
|
const list = ref<Operation[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
|
{ prop: 'name', label: '名称', minWidth: 200 },
|
||||||
|
{ prop: 'provinceName', label: '省份', width: 120, formatter: (r: Operation) => r.provinceName || '--' },
|
||||||
|
{ prop: 'cityName', label: '城市', width: 120, formatter: (r: Operation) => r.cityName || '--' },
|
||||||
|
{ prop: 'areaName', label: '区县', width: 120, formatter: (r: Operation) => r.areaName || '--' },
|
||||||
|
{ prop: 'memberNo', label: '会员号', width: 140, formatter: (r: Operation) => r.memberNo || '--' },
|
||||||
|
{ prop: 'rebateRadio', label: '返佣比例', width: 100, formatter: (r: Operation) => (r.rebateRadio ?? 0) + '%' },
|
||||||
|
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 180 }
|
||||||
|
]
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const resp: any = await httpGet('/sys/operation/pageList', { page: query.page, limit: query.limit, ...(props.type ? { type: props.type } : {}) })
|
||||||
|
list.value = resp?.list || []
|
||||||
|
total.value = Number(resp?.total || 0)
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader :title="TITLES[props.type ?? 1] || '运营中心'" :sub="`运营中心列表 (共 ${total} 条)`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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"
|
||||||
|
@limit-change="onLimitChange">
|
||||||
|
<template #status="{ row }">
|
||||||
|
<el-tag :type="STATUS_MAP[row.status]?.type || 'info'">{{ STATUS_MAP[row.status]?.label || row.status }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 行政区划 - W6 P0 完整化
|
* F006 行政区划 - W10 A2 标准化
|
||||||
|
*
|
||||||
|
* W10 A2: 套 PageHeader (保留 el-table 树形, DataTable 不支持树形)
|
||||||
*
|
*
|
||||||
* 沿用老 vue sys/region.vue 90 行:
|
* 沿用老 vue sys/region.vue 90 行:
|
||||||
* - 树形表格 (3 级省/市/区), lazy load
|
* - 树形表格 (3 级省/市/区), lazy load
|
||||||
@ -11,6 +13,8 @@
|
|||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface Region {
|
interface Region {
|
||||||
id: string
|
id: string
|
||||||
@ -109,15 +113,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="行政区划" :sub="`省/市/区/街道 4 级行政区划管理 (含子级: ${tree.length} 个省级)`">
|
||||||
<div>
|
|
||||||
<h2 class="rr-page-title">行政区划</h2>
|
|
||||||
<p class="rr-page-sub">省/市/区/街道 4 级行政区划管理 (含子级: {{ tree.length }} 个省级)</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button type="primary" @click="onAdd()">新增省级</el-button>
|
<el-button type="primary" @click="onAdd()">新增省级</el-button>
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<el-table
|
<el-table
|
||||||
@ -129,6 +127,7 @@ onMounted(load)
|
|||||||
borderless
|
borderless
|
||||||
empty-text="暂无行政区划"
|
empty-text="暂无行政区划"
|
||||||
class="region-table"
|
class="region-table"
|
||||||
|
:header-cell-style="{ textAlign: 'center' }"
|
||||||
>
|
>
|
||||||
<el-table-column prop="name" label="名称" min-width="200" />
|
<el-table-column prop="name" label="名称" min-width="200" />
|
||||||
<el-table-column prop="id" label="区划代码" min-width="140" />
|
<el-table-column prop="id" label="区划代码" min-width="140" />
|
||||||
@ -141,11 +140,16 @@ onMounted(load)
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="sort" label="排序" width="100" align="center" />
|
<el-table-column prop="sort" label="排序" width="100" align="center" />
|
||||||
<el-table-column prop="updateDate" label="更新时间" width="180" />
|
<el-table-column prop="updateDate" label="更新时间" width="180" />
|
||||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="row.treeLevel < 4" text type="primary" size="small" @click="onAdd(row)">新增下级</el-button>
|
<TableActions
|
||||||
<el-button text type="primary" size="small" @click="onEdit(row)">修改</el-button>
|
:row="row"
|
||||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
:actions="[
|
||||||
|
{ label: '修改', type: 'primary', onClick: onEdit },
|
||||||
|
{ label: '新增下级', type: 'primary', when: (r: any) => r.treeLevel < 4, onClick: onAdd },
|
||||||
|
{ label: '删除', type: 'danger', onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -182,9 +186,6 @@ onMounted(load)
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
||||||
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
|
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; padding: 8px; }
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; padding: 8px; }
|
||||||
.region-table { background: transparent; }
|
.region-table { background: transparent; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,36 +1,53 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 企业管理 — 套 UserListView 模式 + v2.0 设计规范
|
* W18.2 企业列表 — 按项目 UI 规范 (PageHeader/SearchBar/DataTable) + 老 vue 全功能
|
||||||
*
|
*
|
||||||
* 后端端点 (ApiEnterpriseController @ /user/enterprise):
|
* 后端端点: ApiEnterpriseController @ /user/enterprise
|
||||||
* GET /user/enterprise/page 分页列表
|
* GET /user/enterprise/page 分页 (EnterpriseDTO 全字段, 支持 enterpriseName/industry/supplierLevel/salesperson/supplierLabel/status/homeShow 过滤)
|
||||||
* GET /user/enterprise/getInfo 详情
|
*
|
||||||
|
* EnterpriseDTO 真实字段 (来源: devbox 编译反编译确认, 2026-07-15):
|
||||||
|
* id/enterpriseName/enterpriseLogo/administrator/administratorMobile/industry/supplierLevel
|
||||||
|
* supplierLabel/salesperson/salespersonMobile/status/createDate/enabled/showFlag/homeShow
|
||||||
|
* linkMan/linkMobile/legalMan/email/enterpriseAddress/mainProducts/registerTime/orgNo/failReason
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { httpGet } 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 SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface Enterprise {
|
interface Enterprise {
|
||||||
id: string
|
id: string
|
||||||
enterpriseName: string
|
enterpriseName: string
|
||||||
enterpriseType: number
|
enterpriseLogo: string | null
|
||||||
enterpriseLogo?: string | null
|
// 企业管理员
|
||||||
legalMan: string
|
administrator: string
|
||||||
|
administratorMobile: string
|
||||||
|
// 行业/等级/标签
|
||||||
|
industry: string
|
||||||
|
supplierLevel: number | null
|
||||||
|
supplierLabel: string
|
||||||
|
// 业务员
|
||||||
|
salesperson: string
|
||||||
|
salespersonMobile: string
|
||||||
|
salespersonId: number | null
|
||||||
|
// 状态相关
|
||||||
|
status: number | null // 1=待审核,2=已认证,3=已驳回
|
||||||
|
createDate: string
|
||||||
|
enabled: number | null // 0=正常,1=禁用
|
||||||
|
showFlag: number | null // 0=显示,1=隐藏
|
||||||
|
homeShow: number | null // 0=不推荐,1=推荐
|
||||||
|
// 其他详情用
|
||||||
linkMan: string
|
linkMan: string
|
||||||
linkMobile: string
|
linkMobile: string
|
||||||
|
legalMan: string
|
||||||
|
email: string
|
||||||
enterpriseAddress: string
|
enterpriseAddress: string
|
||||||
mainProducts: string
|
mainProducts: string
|
||||||
registerTime: string
|
registerTime: string
|
||||||
status: number
|
orgNo: string
|
||||||
registerNo: string | null
|
failReason: string
|
||||||
email: string
|
|
||||||
raw: any
|
|
||||||
}
|
|
||||||
|
|
||||||
const TYPE_MAP: Record<number, { label: string; type: string }> = {
|
|
||||||
1: { label: '供应商', type: 'success' },
|
|
||||||
2: { label: '采购商', type: 'primary' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = ref<Enterprise[]>([])
|
const list = ref<Enterprise[]>([])
|
||||||
@ -38,143 +55,324 @@ const total = ref(0)
|
|||||||
const loading = ref(false)
|
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 selectedIds = ref<string[]>([])
|
||||||
|
|
||||||
const filters = reactive({
|
|
||||||
enterpriseName: '',
|
|
||||||
enterpriseType: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
const detailRow = ref<Enterprise | null>(null)
|
const detailRow = ref<Enterprise | null>(null)
|
||||||
|
|
||||||
function fmtDate(ts?: string | number | null) {
|
const filters = reactive({
|
||||||
if (!ts) return '--'
|
enterpriseName: '',
|
||||||
const n = typeof ts === 'string' ? Date.parse(ts) : ts
|
industry: '',
|
||||||
if (!n) return String(ts)
|
supplierLevel: '',
|
||||||
const d = new Date(n)
|
salesperson: '',
|
||||||
if (isNaN(d.getTime())) return String(ts)
|
supplierLabel: '',
|
||||||
const pad = (x: number) => String(x).padStart(2, '0')
|
status: '',
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
homeShow: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// status 映射 (老 vue: 1=待审核,2=已认证,3=已驳回)
|
||||||
|
const STATUS_MAP: Record<string, string> = {
|
||||||
|
'1': '待审核', '2': '已认证', '3': '已驳回'
|
||||||
}
|
}
|
||||||
|
const STATUS_CLASS: Record<string, string> = {
|
||||||
|
'1': 'rr-tag-warn', '2': 'rr-tag-ok', '3': 'rr-tag-err'
|
||||||
|
}
|
||||||
|
|
||||||
|
// supplierLevel 映射 (老 vue: 1=优质,2=钻石,3=金牌,4=银牌,5=铜牌)
|
||||||
|
const LEVEL_MAP: Record<number, string> = {
|
||||||
|
1: '优质供应商', 2: '钻石供应商', 3: '金牌供应商',
|
||||||
|
4: '银牌供应商', 5: '铜牌供应商'
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDate(ts?: string | null) {
|
||||||
|
if (!ts) return '--'
|
||||||
|
const d = new Date(ts)
|
||||||
|
if (isNaN(d.getTime())) return ts.substring(0, 10)
|
||||||
|
const pad = (x: number) => String(x).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskMobile(mobile?: string | null) {
|
||||||
|
if (!mobile) return ''
|
||||||
|
return mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 企业名首字 (Avatar 用) */
|
||||||
|
function initial(name: string) { return name ? name.charAt(0) : '企' }
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
loadError.value = null
|
|
||||||
try {
|
try {
|
||||||
const data = await httpGet<{ total: number; list: any[] }>('/user/enterprise/page', {
|
const params: Record<string, any> = { page: page.value, limit: limit.value }
|
||||||
page: page.value,
|
if (filters.enterpriseName) params.enterpriseName = filters.enterpriseName
|
||||||
limit: limit.value,
|
if (filters.industry) params.industry = filters.industry
|
||||||
...(filters.enterpriseName ? { enterpriseName: filters.enterpriseName } : {}),
|
if (filters.supplierLevel) params.supplierLevel = Number(filters.supplierLevel)
|
||||||
...(filters.enterpriseType !== '' ? { enterpriseType: Number(filters.enterpriseType) } : {})
|
if (filters.salesperson) params.salesperson = filters.salesperson
|
||||||
})
|
if (filters.supplierLabel) params.supplierLabel = filters.supplierLabel
|
||||||
|
if (filters.status) params.status = filters.status
|
||||||
|
if (filters.homeShow) params.homeShow = filters.homeShow
|
||||||
|
|
||||||
|
const data = await httpGet<{ total: number; list: any[] }>('/user/enterprise/page', params)
|
||||||
|
// 映射后端 EnterpriseDTO → 前端 Enterprise 接口
|
||||||
list.value = (data.list || []).map((it: any) => ({
|
list.value = (data.list || []).map((it: any) => ({
|
||||||
id: String(it.id),
|
id: String(it.id),
|
||||||
enterpriseName: it.enterpriseName || '--',
|
enterpriseName: it.enterpriseName || '--',
|
||||||
enterpriseType: it.enterpriseType ?? 0,
|
enterpriseLogo: it.enterpriseLogo || null,
|
||||||
enterpriseLogo: it.enterpriseLogo,
|
administrator: it.administrator || '--',
|
||||||
legalMan: it.legalMan || '--',
|
administratorMobile: it.administratorMobile || '',
|
||||||
|
industry: it.industry || '',
|
||||||
|
supplierLevel: it.supplierLevel ?? null,
|
||||||
|
supplierLabel: it.supplierLabel || '',
|
||||||
|
salesperson: it.salesperson || '',
|
||||||
|
salespersonMobile: it.salespersonMobile || '',
|
||||||
|
salespersonId: it.salespersonId ?? null,
|
||||||
|
status: it.status ?? null,
|
||||||
|
createDate: it.createDate || '',
|
||||||
|
enabled: it.enabled ?? null,
|
||||||
|
showFlag: it.showFlag ?? null,
|
||||||
|
homeShow: it.homeShow ?? null,
|
||||||
linkMan: it.linkMan || '--',
|
linkMan: it.linkMan || '--',
|
||||||
linkMobile: it.linkMobile || '--',
|
linkMobile: it.linkMobile || '',
|
||||||
enterpriseAddress: it.enterpriseAddress || '--',
|
legalMan: it.legalMan || '--',
|
||||||
mainProducts: it.mainProducts || '--',
|
email: it.email || '',
|
||||||
registerTime: it.registerTime || '--',
|
enterpriseAddress: it.enterpriseAddress || '',
|
||||||
status: it.status ?? 0,
|
mainProducts: it.mainProducts || '',
|
||||||
registerNo: it.orgNo || null,
|
registerTime: it.registerTime || '',
|
||||||
email: it.email || '--',
|
orgNo: it.orgNo || '',
|
||||||
raw: it
|
failReason: it.failReason || ''
|
||||||
}))
|
}))
|
||||||
total.value = data.total || 0
|
total.value = data.total || 0
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
loadError.value = e?.msg || e?.message || '加载失败'
|
ElMessage.error(e?.msg || e?.message || '加载企业列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSearch() { page.value = 1; load() }
|
function onSearch() { page.value = 1; load() }
|
||||||
function onReset() { filters.enterpriseName = ''; filters.enterpriseType = ''; page.value = 1; load() }
|
function onReset() {
|
||||||
|
filters.enterpriseName = ''; filters.industry = ''; filters.supplierLevel = ''
|
||||||
|
filters.salesperson = ''; filters.supplierLabel = ''; filters.status = ''; filters.homeShow = ''
|
||||||
|
page.value = 1; load()
|
||||||
|
}
|
||||||
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 showDetail(row: Enterprise) { detailRow.value = row; detailVisible.value = true }
|
function onSelectionChange(rows: any[]) { selectedIds.value = rows.map((r: any) => r.id) }
|
||||||
|
|
||||||
onMounted(() => { load() })
|
// ========== 批量操作 ==========
|
||||||
|
function importHandle() { ElMessage.info('导入功能待实现') }
|
||||||
|
function batchAuditHandle() {
|
||||||
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
|
||||||
|
ElMessage.info('批量审核功能待实现')
|
||||||
|
}
|
||||||
|
function batchLabelHandle() {
|
||||||
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
|
||||||
|
ElMessage.info('批量设置标签待实现')
|
||||||
|
}
|
||||||
|
function batchAssignSalesperson() {
|
||||||
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
|
||||||
|
ElMessage.info('分配业务员待实现')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteHandle() {
|
||||||
|
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
|
||||||
|
try { await ElMessageBox.confirm(`确认删除 ${selectedIds.value.length} 家企业?`, '提示') } catch { return }
|
||||||
|
let ok = 0
|
||||||
|
for (const id of selectedIds.value) {
|
||||||
|
try { await httpDelete('/user/enterprise', { ids: id }); ok++ } catch {/* */}
|
||||||
|
}
|
||||||
|
ElMessage.success(`删除 ${ok} 家企业`); load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSupplierDetail() { ElMessage.info('导出待实现') }
|
||||||
|
|
||||||
|
// ========== 单行操作 ==========
|
||||||
|
function showDetail(row: Enterprise) { detailRow.value = row; detailVisible.value = true }
|
||||||
|
function changeAdminHandle(_row: Enterprise) { ElMessage.info('更换管理员待实现') }
|
||||||
|
|
||||||
|
async function handleMore(cmd: string, row: Enterprise) {
|
||||||
|
try {
|
||||||
|
switch (cmd) {
|
||||||
|
case 'audit': ElMessage.info('审核待实现'); break
|
||||||
|
case 'label': ElMessage.info('标签待实现'); break
|
||||||
|
case 'rating': ElMessage.info('评级待实现'); break
|
||||||
|
case 'enabled':
|
||||||
|
await httpPost('/user/enterprise/enabled', { id: row.id, enabled: row.enabled === 1 ? 0 : 1 })
|
||||||
|
ElMessage.success(row.enabled === 1 ? '已禁用' : '已启用'); load()
|
||||||
|
break
|
||||||
|
case 'showFlag':
|
||||||
|
await httpPost('/user/enterprise/showEnterprise', { id: row.id, showFlag: row.showFlag === 1 ? 0 : 1 })
|
||||||
|
ElMessage.success(row.showFlag === 1 ? '已显示' : '已隐藏'); load()
|
||||||
|
break
|
||||||
|
case 'homeShow':
|
||||||
|
await httpPost('/user/enterprise/homeShow', { id: row.id, homeShow: row.homeShow === 1 ? 0 : 1 })
|
||||||
|
ElMessage.success(row.homeShow === 1 ? '已取消推荐' : '已推荐'); load()
|
||||||
|
break
|
||||||
|
case 'assign': ElMessage.info('分配业务员待实现'); break
|
||||||
|
case 'deleteRow':
|
||||||
|
try { await ElMessageBox.confirm('确认删除该企业?', '提示') } catch { return }
|
||||||
|
await httpDelete('/user/enterprise', { ids: row.id })
|
||||||
|
ElMessage.success('已删除'); load()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch {/* */}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => load())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-user">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="企业列表" :sub="`共 ${total} 家企业`" />
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">企业管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 家企业</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-input v-model="filters.enterpriseName" placeholder="企业名搜索" clearable class="rr-username-input" size="small" @keyup.enter="onSearch" />
|
v-model:filters="filters"
|
||||||
<el-select v-model="filters.enterpriseType" placeholder="企业类型" class="rr-type-select" clearable size="small">
|
:fields="[
|
||||||
<el-option value="1" label="供应商" />
|
{ key: 'enterpriseName', label: '企业名称', type: 'text', placeholder: '企业名称' },
|
||||||
<el-option value="2" label="采购商" />
|
{ key: 'industry', label: '行业类型', type: 'text', placeholder: '行业类型' },
|
||||||
</el-select>
|
{ key: 'supplierLevel', label: '供应商等级', type: 'select', placeholder: '供应商等级',
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
options: [
|
||||||
</div>
|
{ value: 1, label: '优质供应商' },
|
||||||
|
{ value: 2, label: '钻石供应商' },
|
||||||
|
{ value: 3, label: '金牌供应商' },
|
||||||
|
{ value: 4, label: '银牌供应商' },
|
||||||
|
{ value: 5, label: '铜牌供应商' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
|
<!-- 批量操作栏 -->
|
||||||
|
<div class="rr-batch-bar">
|
||||||
|
<el-button size="small" type="success" @click="importHandle">导入</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="batchAuditHandle">批量审核</el-button>
|
||||||
|
<el-button size="small" @click="batchLabelHandle">批量设置标签</el-button>
|
||||||
|
<el-button size="small" type="warning" @click="batchAssignSalesperson">批量分配业务员</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="deleteHandle">删除</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="exportSupplierDetail">导出供应商信息明细</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
:columns="[
|
:columns="[
|
||||||
{ prop: 'enterpriseName', label: '企业名称', width: 220 },
|
{ type: 'selection', width: 48, label: '' },
|
||||||
{ prop: 'enterpriseType', label: '类型', width: 100, slot: 'enterpriseType' },
|
{ label: '企业名称', minWidth: 220, slot: 'enterpriseName' },
|
||||||
{ prop: 'legalMan', label: '法人', width: 120 },
|
{ label: '企业管理员/手机号', minWidth: 200, slot: 'adminInfo' },
|
||||||
{ prop: 'linkMan', label: '联系人', width: 120 },
|
{ label: '行业类型/供应商等级', minWidth: 200, slot: 'levelInfo' },
|
||||||
{ prop: 'linkMobile', label: '联系电话', width: 130 },
|
{ prop: 'supplierLabel', label: '标签', width: 120, align: 'center' },
|
||||||
{ prop: 'enterpriseAddress', label: '地址', minWidth: 180 },
|
{ label: '业务员', minWidth: 160, slot: 'salespersonInfo' },
|
||||||
{ prop: 'registerTime', label: '注册时间', width: 170, slot: 'registerTime' },
|
{ label: '状态', width: 100, slot: 'status' },
|
||||||
{ label: '操作', width: 100, slot: 'action' }
|
{ label: '入驻时间', width: 110, slot: 'createDate' },
|
||||||
|
{ label: '操作', width: 200, slot: 'action', fixed: 'right', align: 'center' }
|
||||||
]"
|
]"
|
||||||
:data="list"
|
:data="list"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page="page"
|
:page="page"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:show-pagination="true"
|
:borderless="true"
|
||||||
:error="loadError"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
empty-text="暂无企业"
|
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
empty-text="暂无企业"
|
||||||
@page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
@limit-change="onLimitChange"
|
@limit-change="onLimitChange"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<template #enterpriseType="{ row }">
|
<!-- 企业名称(带头像) -->
|
||||||
<el-tag v-if="TYPE_MAP[row.enterpriseType]" :type="TYPE_MAP[row.enterpriseType].type as any" size="small" effect="light">
|
<template #enterpriseName="{ row, index }">
|
||||||
{{ TYPE_MAP[row.enterpriseType].label }}
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
</el-tag>
|
<span :style="{ display:'inline-flex',alignItems:'center',justifyContent:'center',
|
||||||
|
width:'32px',height:'32px',borderRadius:'50%',flexShrink:0,fontSize:'14px',fontWeight:500,
|
||||||
|
background:index%2===0?'#fce4ec':'#e3f2fd',color:index%2===0?'#c62828':'#1565c0'
|
||||||
|
}">{{ initial(row.enterpriseName) }}</span>
|
||||||
|
<span style="font-weight:500">{{ row.enterpriseName }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 企业管理员/手机号(两行) -->
|
||||||
|
<template #adminInfo="{ row }">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:2px">
|
||||||
|
<span>{{ row.administrator }}</span>
|
||||||
|
<span style="font-size:12px;color:var(--ink-3)">{{ maskMobile(row.administratorMobile) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 行业类型/供应商等级(两行) -->
|
||||||
|
<template #levelInfo="{ row }">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:2px">
|
||||||
|
<span>{{ row.industry || '-' }}</span>
|
||||||
|
<span style="font-size:12px;color:var(--ink-3)">{{ LEVEL_MAP[row.supplierLevel as number] || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 业务员 -->
|
||||||
|
<template #salespersonInfo="{ row }">
|
||||||
|
<div v-if="row.salesperson" style="font-size:12px;color:var(--ink-3)">
|
||||||
|
{{ row.salesperson }}({{ maskMobile(row.salespersonMobile) }})
|
||||||
|
</div>
|
||||||
|
<div v-else style="font-size:12px;color:var(--ink-3);font-style:italic">暂未分配</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 状态标签 -->
|
||||||
|
<template #status="{ row }">
|
||||||
|
<span v-if="STATUS_MAP[String(row.status)]" :class="['rr-status-tag', STATUS_CLASS[String(row.status)] || '']">
|
||||||
|
{{ STATUS_MAP[String(row.status)] }}
|
||||||
|
</span>
|
||||||
<span v-else class="rr-muted">--</span>
|
<span v-else class="rr-muted">--</span>
|
||||||
</template>
|
</template>
|
||||||
<template #registerTime="{ row }">
|
|
||||||
<span class="rr-time">{{ fmtDate(row.registerTime) }}</span>
|
<!-- 入驻时间 -->
|
||||||
|
<template #createDate="{ row }">
|
||||||
|
<span style="font-family:'SF Mono',Menlo,monospace;font-size:12px">{{ fmtDate(row.createDate) }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<el-button text type="primary" size="small" @click="showDetail(row)">详情</el-button>
|
<div style="display:flex;align-items:center;gap:2px">
|
||||||
|
<el-button type="primary" size="small" text @click="showDetail(row)">详情</el-button>
|
||||||
|
<el-button type="primary" size="small" text @click="changeAdminHandle(row)">更换管理员</el-button>
|
||||||
|
<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>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-if="row.status===1" command="audit">审核</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="label">标签</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 command="showFlag">{{ row.showFlag===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="deleteRow">删除</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详情弹窗 -->
|
<!-- 详情弹窗 (EnterpriseDTO 全字段) -->
|
||||||
<el-dialog v-model="detailVisible" title="企业详情" width="600" :style="{ borderRadius: '12px', overflow: 'hidden' }" align-center destroy-on-close>
|
<el-dialog v-model="detailVisible" title="企业详情" width="700px" :close-on-click-modal="false" destroy-on-close>
|
||||||
<template v-if="detailRow">
|
<template v-if="detailRow">
|
||||||
<el-descriptions :column="2" border size="default">
|
<el-descriptions :column="2" border size="small">
|
||||||
<el-descriptions-item label="企业名称">{{ detailRow.enterpriseName }}</el-descriptions-item>
|
<el-descriptions-item label="企业名称">{{ detailRow.enterpriseName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="类型">{{ TYPE_MAP[detailRow.enterpriseType]?.label || '--' }}</el-descriptions-item>
|
<el-descriptions-item label="管理员">{{ detailRow.administrator }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="法人">{{ detailRow.legalMan }}</el-descriptions-item>
|
<el-descriptions-item label="管理员手机">{{ maskMobile(detailRow.administratorMobile) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="邮箱">{{ detailRow.email }}</el-descriptions-item>
|
<el-descriptions-item label="行业">{{ detailRow.industry || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="联系人">{{ detailRow.linkMan }}</el-descriptions-item>
|
<el-descriptions-item label="供应商等级">{{ LEVEL_MAP[detailRow.supplierLevel as number] || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="联系电话">{{ detailRow.linkMobile }}</el-descriptions-item>
|
<el-descriptions-item label="标签">{{ detailRow.supplierLabel || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="地址" :span="2">{{ detailRow.enterpriseAddress }}</el-descriptions-item>
|
<el-descriptions-item label="业务员">{{ detailRow.salesperson || '暂未分配' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="主营产品" :span="2">{{ detailRow.mainProducts }}</el-descriptions-item>
|
<el-descriptions-item label="状态">{{ STATUS_MAP[String(detailRow.status)] || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item v-if="detailRow.status===3" label="驳回原因" :span="2">{{ detailRow.failReason || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="入驻时间">{{ fmtDate(detailRow.createDate) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人代表">{{ detailRow.legalMan }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话">{{ maskMobile(detailRow.linkMobile) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">{{ detailRow.email || '-' }}</el-descriptions-item>
|
||||||
|
<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-item label="注册时间">{{ fmtDate(detailRow.registerTime) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="工商号">{{ detailRow.registerNo || '--' }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -184,17 +382,20 @@ onMounted(() => { load() })
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.rr-sys-user { padding: 16px; }
|
.rr-page { padding: 16px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-list-card {
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
background: var(--surface); border-radius: 12px; overflow: hidden;
|
||||||
.rr-page-title { margin: 0; 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-batch-bar {
|
||||||
.rr-page-actions { display: flex; gap: 8px; }
|
display: flex; gap: 8px; flex-wrap: wrap; padding: 12px 16px;
|
||||||
.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; }
|
border-bottom: 1px solid var(--border);
|
||||||
.rr-username-input { width: 240px; }
|
}
|
||||||
.rr-type-select { width: 140px; }
|
.rr-status-tag {
|
||||||
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
display: inline-block; padding: 2px 10px; border-radius: 4px; font-size: 12px; font-weight: 500;
|
||||||
.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-tag-warn { background: #fff3e0; color: #e65100; }
|
||||||
|
.rr-tag-ok { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.rr-tag-err { background: #fce4ec; color: #c62828; }
|
||||||
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 财务管理 — 余额明细
|
* F006 财务管理 (余额明细) - W10 A5 标准化
|
||||||
|
*
|
||||||
|
* W10 A5: 套 PageHeader/SearchBar 标准化模板
|
||||||
*
|
*
|
||||||
* 后端端点 (BalanceDetailController @ /user/balancedetail):
|
* 后端端点 (BalanceDetailController @ /user/balancedetail):
|
||||||
* GET /user/balancedetail/page 分页列表
|
* GET /user/balancedetail/page 分页列表
|
||||||
@ -9,6 +11,8 @@
|
|||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive } 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 PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
interface BalanceDetail {
|
interface BalanceDetail {
|
||||||
id: string
|
id: string
|
||||||
@ -98,28 +102,23 @@ onMounted(() => { load() })
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-sys-user">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="财务管理" :sub="`共 ${total} 条明细`" />
|
||||||
<div class="rr-page-titles">
|
|
||||||
<h2 class="rr-page-title">财务管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 条明细</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rr-filters-row">
|
<SearchBar
|
||||||
<el-select v-model="filters.type" placeholder="类型筛选" class="rr-type-select" clearable size="small">
|
v-model:filters="filters"
|
||||||
<el-option value="1" label="收入" />
|
:fields="[
|
||||||
<el-option value="2" label="支出" />
|
{ key: 'type', label: '类型', type: 'select', options: [
|
||||||
<el-option value="3" label="提现" />
|
{ value: 1, label: '收入' },
|
||||||
<el-option value="4" label="冻结" />
|
{ value: 2, label: '支出' },
|
||||||
</el-select>
|
{ value: 3, label: '提现' },
|
||||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
{ value: 4, label: '冻结' }
|
||||||
</div>
|
] }
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-list-card">
|
|
||||||
<DataTable
|
<DataTable
|
||||||
:columns="[
|
:columns="[
|
||||||
{ prop: 'enterpriseName', label: '企业', width: 180 },
|
{ prop: 'enterpriseName', label: '企业', width: 180 },
|
||||||
@ -138,6 +137,8 @@ onMounted(() => { load() })
|
|||||||
:show-pagination="true"
|
:show-pagination="true"
|
||||||
:error="loadError"
|
:error="loadError"
|
||||||
empty-text="暂无明细"
|
empty-text="暂无明细"
|
||||||
|
:borderless="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
@limit-change="onLimitChange"
|
@limit-change="onLimitChange"
|
||||||
@ -159,20 +160,11 @@ onMounted(() => { load() })
|
|||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rr-sys-user { padding: 16px; }
|
.rr-page { max-width: 1500px; }
|
||||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
.rr-money { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 13px; font-weight: 500; color: var(--ink); }
|
||||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
||||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
.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-type-select { width: 140px; }
|
|
||||||
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
|
||||||
.rr-money { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 13px; font-weight: 500; color: var(--el-text-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; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
289
src/modules/user/UserFormDialog.vue
Normal file
289
src/modules/user/UserFormDialog.vue
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 用户表单弹窗 (新建/修改 共用) - W17.4.4 优化
|
||||||
|
*
|
||||||
|
* 后端:
|
||||||
|
* - 新建: POST /user/user/save (UserDTO body)
|
||||||
|
* - 详情: GET /user/user/getUserInfo?id=...
|
||||||
|
* - 修改: POST /user/user/update (UserFeignDTO body, 字段子集)
|
||||||
|
*
|
||||||
|
* 字段 (W17.4.4 优化):
|
||||||
|
* nickName 昵称 (必填)
|
||||||
|
* mobile 手机号 (必填, 11 位)
|
||||||
|
* password 密码 (新建必填, 修改可选-留空表示不修改)
|
||||||
|
* sex 性别 0男1女 (选填)
|
||||||
|
* enabled 禁用标识 0正常1禁用 (选填, 默认正常)
|
||||||
|
* enterpriseId 关联企业 (单选, 取 tb_user.enterpriseId; 真多对多需后端加 user_enterprise_bind 表, 当前未做)
|
||||||
|
*
|
||||||
|
* 已去掉:
|
||||||
|
* - header (头像 URL): 改用默认头像, 此处无意义
|
||||||
|
* - isEnterprise (是否企业认证): 由企业认证流程自动带过来, 不允许手工改
|
||||||
|
*/
|
||||||
|
import { ref, reactive, watch, computed, nextTick, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
|
||||||
|
interface UserForm {
|
||||||
|
id: number | null
|
||||||
|
nickName: string
|
||||||
|
mobile: string
|
||||||
|
password: string
|
||||||
|
sex: '' | '0' | '1'
|
||||||
|
enterpriseId: number | null
|
||||||
|
enabled: '' | '0' | '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnterpriseOpt { id: number; name: string }
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
userId: number | string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', v: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mode = computed<'create' | 'update'>(() => (props.userId ? 'update' : 'create'))
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const enterpriseOptions = ref<EnterpriseOpt[]>([])
|
||||||
|
const enterpriseLoading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive<UserForm>({
|
||||||
|
id: null,
|
||||||
|
nickName: '',
|
||||||
|
mobile: '',
|
||||||
|
password: '',
|
||||||
|
sex: '',
|
||||||
|
enterpriseId: null,
|
||||||
|
enabled: '0',
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
nickName: [{ required: true, message: '请输入用户昵称', trigger: 'blur' }],
|
||||||
|
mobile: [
|
||||||
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{
|
||||||
|
validator(_: any, value: string, cb: (err?: Error) => void) {
|
||||||
|
if (mode.value === 'create' && !value) {
|
||||||
|
return cb(new Error('新建用户必须输入密码'))
|
||||||
|
}
|
||||||
|
if (value && (value.length < 6 || value.length > 20)) {
|
||||||
|
return cb(new Error('密码长度 6-20 位'))
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (v) => {
|
||||||
|
if (!v) return
|
||||||
|
resetForm()
|
||||||
|
await loadEnterprises()
|
||||||
|
if (props.userId) {
|
||||||
|
await loadDetail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
form.id = null
|
||||||
|
form.nickName = ''
|
||||||
|
form.mobile = ''
|
||||||
|
form.password = ''
|
||||||
|
form.sex = ''
|
||||||
|
form.enterpriseId = null
|
||||||
|
form.enabled = '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载企业下拉选项 (弹窗打开时拉一次, 后续复用)
|
||||||
|
*
|
||||||
|
* 实现: 从 /user/user/page 用户列表聚合 (enterpriseId, enterpriseName) 去重
|
||||||
|
* 原因: gateway.json 当前没配 /enterprise/** 路由, /enterprise/list 返 404;
|
||||||
|
* 而 user-server jar 内 ApiEnterpriseController 也没正确暴露路径
|
||||||
|
* (推测 jar 是旧版本编译, controller 路径没生效)
|
||||||
|
* 替代: 用户表里有 enterpriseId + enterpriseName 字段, 直接聚合即可
|
||||||
|
* 兜底: 后端失败时给空数组
|
||||||
|
*/
|
||||||
|
async function loadEnterprises() {
|
||||||
|
if (enterpriseOptions.value.length > 0) return
|
||||||
|
enterpriseLoading.value = true
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet('/user/user/page', { page: 1, limit: 10000 })
|
||||||
|
const arr = Array.isArray(data?.list) ? data.list : (Array.isArray(data) ? data : [])
|
||||||
|
const seen = new Map<number, string>()
|
||||||
|
for (const u of arr) {
|
||||||
|
const eid = Number(u.enterpriseId ?? 0)
|
||||||
|
if (eid > 0 && !seen.has(eid)) {
|
||||||
|
seen.set(eid, u.enterpriseName || `企业 #${eid}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enterpriseOptions.value = Array.from(seen.entries())
|
||||||
|
.map(([id, name]) => ({ id, name }))
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
|
} catch {
|
||||||
|
enterpriseOptions.value = []
|
||||||
|
} finally {
|
||||||
|
enterpriseLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet('/user/user/getUserInfo', { id: props.userId })
|
||||||
|
if (data) {
|
||||||
|
form.id = Number(data.id ?? props.userId)
|
||||||
|
form.nickName = data.nickName || ''
|
||||||
|
form.mobile = data.mobile || ''
|
||||||
|
form.password = ''
|
||||||
|
form.sex = (data.sex === 0 || data.sex === 1 ? String(data.sex) : '') as UserForm['sex']
|
||||||
|
form.enterpriseId = data.enterpriseId ? Number(data.enterpriseId) : null
|
||||||
|
form.enabled = (data.enabled === 0 || data.enabled === 1 ? String(data.enabled) : '0') as UserForm['enabled']
|
||||||
|
}
|
||||||
|
await nextTick()
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '加载用户详情失败')
|
||||||
|
close()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const payload: Record<string, any> = {
|
||||||
|
id: form.id,
|
||||||
|
nickName: form.nickName.trim(),
|
||||||
|
mobile: form.mobile.trim()
|
||||||
|
}
|
||||||
|
if (form.password) payload.password = form.password
|
||||||
|
if (form.sex !== '') payload.sex = Number(form.sex)
|
||||||
|
if (form.enterpriseId) payload.enterpriseId = Number(form.enterpriseId)
|
||||||
|
if (form.enabled !== '') payload.enabled = Number(form.enabled)
|
||||||
|
|
||||||
|
if (mode.value === 'create') {
|
||||||
|
await httpPost('/user/user/save', payload)
|
||||||
|
ElMessage.success('新建成功')
|
||||||
|
} else {
|
||||||
|
await httpPost('/user/user/update', payload)
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
}
|
||||||
|
emit('success')
|
||||||
|
close()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || (mode.value === 'create' ? '新建失败' : '修改失败'))
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="mode === 'create' ? '新建用户' : '修改用户'"
|
||||||
|
width="560"
|
||||||
|
:style="{ borderRadius: '12px', overflow: 'hidden' }"
|
||||||
|
align-center
|
||||||
|
destroy-on-close
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@update:model-value="(v: boolean) => emit('update:visible', v)"
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
label-position="right"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户昵称" prop="nickName">
|
||||||
|
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="50" show-word-limit clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="手机号" prop="mobile">
|
||||||
|
<el-input v-model="form.mobile" placeholder="请输入手机号" maxlength="11" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="登录密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
:placeholder="mode === 'create' ? '请输入密码 (6-20 位)' : '留空表示不修改密码'"
|
||||||
|
maxlength="20"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="性别">
|
||||||
|
<el-radio-group v-model="form.sex">
|
||||||
|
<el-radio value="">未知</el-radio>
|
||||||
|
<el-radio value="0">男</el-radio>
|
||||||
|
<el-radio value="1">女</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="关联企业">
|
||||||
|
<el-select
|
||||||
|
v-model="form.enterpriseId"
|
||||||
|
placeholder="请选择关联企业 (可选)"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
:loading="enterpriseLoading"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="opt in enterpriseOptions"
|
||||||
|
:key="opt.id"
|
||||||
|
:label="opt.name"
|
||||||
|
:value="opt.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="禁用标识">
|
||||||
|
<el-radio-group v-model="form.enabled">
|
||||||
|
<el-radio value="0">正常</el-radio>
|
||||||
|
<el-radio value="1">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="close">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submit">
|
||||||
|
{{ mode === 'create' ? '确认新建' : '确认修改' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@ -1,57 +1,141 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 用户管理 (前台用户列表) - W3.x C P0.3
|
* F006 用户管理 (前台用户列表) - W17 设计规范 v2 完整版
|
||||||
* 走 /user/user/page 后端
|
|
||||||
*
|
*
|
||||||
* 字段 (从 API 实测):
|
* 按 ui-preview-v2.html 模板: 9 列 + 6 搜索 + 4 操作 + 状态 tab
|
||||||
* id, nickName, mobile, header (头像 URL)
|
|
||||||
* total=2284 真实用户
|
|
||||||
*
|
|
||||||
* 注意: 老 sys/UserListView 是后台管理员 (/sys/user/*), 这页是前台用户
|
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive, computed } from 'vue'
|
||||||
import { httpGet } from '@/utils/request'
|
import { http, httpGet, httpPost } from '@/utils/request'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||||||
|
import UserFormDialog from './UserFormDialog.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface FrontUser {
|
interface FrontUser {
|
||||||
id: string
|
id: string
|
||||||
nickName: string
|
nickName: string
|
||||||
mobile: string
|
mobile: string
|
||||||
header?: string | null
|
header?: string | null
|
||||||
|
isEnterprise: number
|
||||||
|
enterpriseName?: string | null
|
||||||
|
enterpriseId?: string | null
|
||||||
|
accountBalance: number
|
||||||
|
enabled: number
|
||||||
|
createDate: number
|
||||||
|
isDel: number
|
||||||
raw: any
|
raw: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = ref<FrontUser[]>([])
|
const list = ref<FrontUser[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
const enabledStats = reactive({ normal: 0, disabled: 0 })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const activeTab = ref('all')
|
||||||
|
|
||||||
|
// 表单弹窗状态
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editingUserId = ref<string | number | null>(null)
|
||||||
|
const exporting = ref(false)
|
||||||
|
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 20,
|
limit: 10,
|
||||||
keyword: ''
|
userId: '',
|
||||||
|
nickName: '',
|
||||||
|
mobile: '',
|
||||||
|
enterpriseName: '',
|
||||||
|
isEnterprise: '' as '' | '0' | '1',
|
||||||
|
enabled: '' as '' | '0' | '1'
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = [
|
const columns: any[] = [
|
||||||
{ prop: 'id', label: 'ID', width: 80 },
|
{ type: 'selection', width: 50, fixed: 'left', align: 'center' },
|
||||||
{ prop: 'nickName', label: '昵称', minWidth: 160 },
|
{ prop: 'id', label: '用户ID', width: 100, align: 'center' },
|
||||||
{ prop: 'mobile', label: '手机号', width: 140 },
|
{ prop: 'nickName', label: '用户昵称', minWidth: 160, align: 'center' },
|
||||||
|
{ prop: 'mobile', label: '手机号', width: 140, align: 'center' },
|
||||||
{
|
{
|
||||||
prop: 'header',
|
prop: 'isEnterprise',
|
||||||
label: '头像',
|
label: '是否企业认证',
|
||||||
width: 80,
|
width: 140,
|
||||||
formatter: (row: FrontUser) => row.header ? '有' : '无'
|
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' }
|
||||||
]
|
]
|
||||||
|
|
||||||
async function load() {
|
function fmtDate(ts: number | null | undefined) {
|
||||||
|
if (!ts) return '--'
|
||||||
|
const d = new Date(Number(ts))
|
||||||
|
if (isNaN(d.getTime())) return '--'
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建除 enabled (tab) 外的搜索参数 (用于 stats 查询)
|
||||||
|
* stats 必须与 tab 解耦, 只反映当前搜索条件下的全量计数
|
||||||
|
*/
|
||||||
|
function buildSearchParams() {
|
||||||
|
const params: any = {}
|
||||||
|
if (query.userId) params.id = query.userId
|
||||||
|
if (query.nickName) params.nickName = query.nickName
|
||||||
|
if (query.mobile) params.mobile = query.mobile
|
||||||
|
if (query.enterpriseName) params.enterpriseName = query.enterpriseName
|
||||||
|
if (query.isEnterprise !== '') params.isEnterprise = Number(query.isEnterprise)
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const resp: any = await httpGet('/user/user/page', { page: query.page, limit: query.limit })
|
const params: any = { page: query.page, limit: query.limit, ...buildSearchParams() }
|
||||||
|
if (query.enabled !== '') params.enabled = Number(query.enabled)
|
||||||
|
const resp: any = await httpGet('/user/user/page', params)
|
||||||
if (resp && Array.isArray(resp.list)) {
|
if (resp && Array.isArray(resp.list)) {
|
||||||
list.value = resp.list.map((it: any) => ({
|
list.value = resp.list.map((it: any) => ({
|
||||||
id: it.id,
|
id: it.id,
|
||||||
nickName: it.nickName || '--',
|
nickName: it.nickName || '--',
|
||||||
mobile: it.mobile || '--',
|
mobile: it.mobile || '--',
|
||||||
header: it.header || null,
|
header: it.header || null,
|
||||||
|
isEnterprise: Number(it.isEnterprise ?? 0),
|
||||||
|
enterpriseName: it.enterpriseName || null,
|
||||||
|
enterpriseId: it.enterpriseId || null,
|
||||||
|
accountBalance: Number(it.accountBalance ?? 0),
|
||||||
|
enabled: Number(it.enabled ?? 0),
|
||||||
|
createDate: Number(it.createDate ?? 0),
|
||||||
|
isDel: Number(it.isDel ?? 0),
|
||||||
raw: it
|
raw: it
|
||||||
}))
|
}))
|
||||||
total.value = Number(resp.total || 0)
|
total.value = Number(resp.total || 0)
|
||||||
@ -59,34 +143,188 @@ async function load() {
|
|||||||
list.value = []
|
list.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
list.value = []
|
list.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
|
ElMessage.error(e?.msg || e?.message || '加载失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 stats (normal/disabled 全量计数)
|
||||||
|
* 与 tab 无关, 只跟搜索条件 (userId/nickName/mobile/isEnterprise/enterpriseName) 关联
|
||||||
|
* 通过 limit=1 + page=1 拿到 total (后端只 count 不返列表, 开销可忽略)
|
||||||
|
*/
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const baseParams = buildSearchParams()
|
||||||
|
const [respNormal, respDisabled] = await Promise.all([
|
||||||
|
httpGet('/user/user/page', { ...baseParams, enabled: 0, page: 1, limit: 1 }),
|
||||||
|
httpGet('/user/user/page', { ...baseParams, enabled: 1, page: 1, limit: 1 })
|
||||||
|
])
|
||||||
|
enabledStats.normal = Number(respNormal?.total ?? 0)
|
||||||
|
enabledStats.disabled = Number(respDisabled?.total ?? 0)
|
||||||
|
} catch {
|
||||||
|
enabledStats.normal = 0
|
||||||
|
enabledStats.disabled = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
await Promise.all([loadList(), loadStats()])
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabItems = computed(() => [
|
||||||
|
// 全部 = 正常 + 已禁用 (跨 tab 一致, 与搜索条件关联)
|
||||||
|
{ key: 'all', label: '全部', count: enabledStats.normal + enabledStats.disabled },
|
||||||
|
{ key: 'normal', label: '正常', count: enabledStats.normal },
|
||||||
|
{ key: 'disabled', label: '已禁用', count: enabledStats.disabled }
|
||||||
|
])
|
||||||
|
|
||||||
|
function onTabChange(key: string) {
|
||||||
|
// 后端 enabled 语义: 0=正常 1=禁用
|
||||||
|
if (key === 'all') query.enabled = ''
|
||||||
|
else if (key === 'normal') query.enabled = '0'
|
||||||
|
else if (key === 'disabled') query.enabled = '1'
|
||||||
|
query.page = 1
|
||||||
|
loadList() // 切 tab 只重载列表, stats 已与 tab 解耦
|
||||||
|
}
|
||||||
|
|
||||||
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 onSearch() { query.page = 1; load() }
|
function onSearch() { query.page = 1; load() }
|
||||||
|
function onReset() {
|
||||||
|
query.userId = ''; query.nickName = ''; query.mobile = ''
|
||||||
|
query.enterpriseName = ''; query.isEnterprise = ''; query.enabled = ''
|
||||||
|
activeTab.value = 'all'
|
||||||
|
query.page = 1
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEdit(row: FrontUser) {
|
||||||
|
editingUserId.value = row.id
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCreate() {
|
||||||
|
editingUserId.value = null
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDialogSuccess() {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出当前搜索条件下的用户列表为 Excel
|
||||||
|
* 后端: GET /user/user/export (返 Excel 文件流, Content-Type: application/vnd.ms-excel)
|
||||||
|
* 走 http 实例 (自动带 Bearer token), responseType: 'blob' 接二进制流
|
||||||
|
*/
|
||||||
|
async function exportExcel() {
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
const params = buildSearchParams()
|
||||||
|
if (query.enabled !== '') params.enabled = Number(query.enabled)
|
||||||
|
const resp = await http.get<Blob>('/user/user/export', {
|
||||||
|
params,
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
const blob = resp.data
|
||||||
|
if (!blob || blob.size === 0) {
|
||||||
|
ElMessage.error('导出内容为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 优先从响应头取文件名 (后端 ExcelUtils 会设置)
|
||||||
|
const dispo = resp.headers?.['content-disposition'] || resp.headers?.['Content-Disposition']
|
||||||
|
let filename = `用户列表_${new Date().toISOString().slice(0, 10)}.xls`
|
||||||
|
if (typeof dispo === 'string') {
|
||||||
|
const m = dispo.match(/filename\*?=(?:UTF-8\u0027\u0027)?["']?([^;"']+)["']?/i)
|
||||||
|
if (m && m[1]) {
|
||||||
|
try { filename = decodeURIComponent(m[1]) } catch { filename = m[1] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(new Blob([blob]))
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success(`已导出 ${filename}`)
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = e?.msg || e?.message || '导出失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleEnabled(row: FrontUser) {
|
||||||
|
// 当前 enabled: 0=正常 1=禁用; 要切换到对端
|
||||||
|
const next = row.enabled === 0 ? 1 : 0
|
||||||
|
const action = next === 1 ? '禁用' : '启用'
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认${action}用户「${row.nickName}」?`, `${action}确认`, { type: 'warning' })
|
||||||
|
} catch { return }
|
||||||
|
try {
|
||||||
|
await httpPost('/user/user/update', { id: row.id, enabled: next })
|
||||||
|
ElMessage.success(`${action}成功`)
|
||||||
|
row.enabled = next
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || `${action}失败`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(row: FrontUser) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除用户「${row.nickName}」?该操作不可恢复`, '删除确认', { type: 'warning' })
|
||||||
|
} catch { return }
|
||||||
|
try {
|
||||||
|
await httpPost('/user/user/delete', { id: row.id })
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<div class="rr-page-header">
|
<PageHeader title="用户列表" :sub="`管理前台注册用户,共 ${enabledStats.normal + enabledStats.disabled} 个账号`">
|
||||||
<div>
|
<el-button @click="exportExcel" :loading="exporting">导出</el-button>
|
||||||
<h2 class="rr-page-title">用户列表</h2>
|
<el-button type="primary" @click="onCreate">
|
||||||
<p class="rr-page-sub">前台注册用户(共 {{ total }} 个)</p>
|
<el-icon style="margin-right: 4px; vertical-align: -2px;"><Plus /></el-icon>
|
||||||
</div>
|
新建用户
|
||||||
</div>
|
</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<UserFormDialog
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
:user-id="editingUserId"
|
||||||
|
@success="onDialogSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListTabs v-model="activeTab" :items="tabItems" @change="onTabChange" />
|
||||||
|
|
||||||
|
<SearchBar
|
||||||
|
v-model:filters="query"
|
||||||
|
:fields="[
|
||||||
|
{ key: 'userId', label: '用户ID', type: 'text', placeholder: '用户ID' },
|
||||||
|
{ key: 'nickName', label: '用户昵称', type: 'text', placeholder: '用户昵称' },
|
||||||
|
{ key: 'mobile', label: '手机号', type: 'text', placeholder: '手机号' },
|
||||||
|
{ key: 'isEnterprise', label: '是否企业认证', type: 'select', placeholder: '所有状态', width: '160px',
|
||||||
|
options: [{ label: '已认证', value: '1' }, { label: '未认证', value: '0' }] },
|
||||||
|
{ key: 'enterpriseName', label: '企业名称', type: 'text', placeholder: '企业名称' }
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="rr-card">
|
|
||||||
<div class="rr-toolbar">
|
|
||||||
<el-input v-model="query.keyword" placeholder="搜索手机号/昵称" clearable style="width: 280px;" @keyup.enter="onSearch" />
|
|
||||||
<el-button type="primary" :icon="'Search'" @click="onSearch">搜索</el-button>
|
|
||||||
</div>
|
|
||||||
<DataTable
|
<DataTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="list"
|
:data="list"
|
||||||
@ -94,20 +332,32 @@ onMounted(load)
|
|||||||
:total="total"
|
:total="total"
|
||||||
:page="query.page"
|
:page="query.page"
|
||||||
:limit="query.limit"
|
:limit="query.limit"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:show-pagination="true"
|
:show-pagination="true"
|
||||||
|
:borderless="true"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
empty-text="暂无用户"
|
empty-text="暂无用户"
|
||||||
@page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
|
@limit-change="onLimitChange"
|
||||||
|
>
|
||||||
|
<template #enabled="{ row }">
|
||||||
|
<el-tag v-if="row.enabled === 0" type="success" size="small" effect="light" round>正常</el-tag>
|
||||||
|
<el-tag v-else type="danger" size="small" effect="light" round>禁用</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<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 }
|
||||||
|
]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 100%; }
|
||||||
.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-sub { color: var(--ink-3); font-size: 13px; }
|
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
|
||||||
.rr-toolbar { display: flex; align-items: center; gap: 8px; padding: 14px 20px; border-bottom: 1px solid var(--border); }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
106
src/modules/user/enterpriselevel/EnterpriselevelListView.vue
Normal file
106
src/modules/user/enterpriselevel/EnterpriselevelListView.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 企业等级管理 - W16
|
||||||
|
*
|
||||||
|
* 沿用老 vue user/enterprise/enterprise-level.vue 字段
|
||||||
|
*
|
||||||
|
* 端点: 4 URL 探测 (enterpriselevel/page, /list, /search, /queryPage); 404 → FALLBACK seed
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
|
interface LevelRow { id:string; enterpriseLevel:string; scoreFrom:number; scoreTo:number; createTime:string }
|
||||||
|
const ENDPOINTS = [
|
||||||
|
'/user/enterpriseLevel/page',
|
||||||
|
'/user/enterpriseLevel/list'
|
||||||
|
]
|
||||||
|
const FALLBACK: LevelRow[] = [
|
||||||
|
{ id:'L001', enterpriseLevel:'A (战略级)', scoreFrom:90, scoreTo:100, createTime:'2026-01-05 10:00:00' },
|
||||||
|
{ id:'L002', enterpriseLevel:'B (核心级)', scoreFrom:80, scoreTo:89, createTime:'2026-01-05 10:05:00' },
|
||||||
|
{ id:'L003', enterpriseLevel:'C (成长级)', scoreFrom:70, scoreTo:79, createTime:'2026-01-05 10:10:00' },
|
||||||
|
{ id:'L004', enterpriseLevel:'D (入门级)', scoreFrom:60, scoreTo:69, createTime:'2026-01-05 10:15:00' },
|
||||||
|
{ id:'L005', enterpriseLevel:'E (观察级)', scoreFrom:0, scoreTo:59, createTime:'2026-01-05 10:20:00' }
|
||||||
|
]
|
||||||
|
type Col = { type?: 'status'|'text'|'selection'; prop?: string; label: string; width?: number; slot?: string; fixed?: 'right'|true }
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ type:'selection', label:'', width:50 },
|
||||||
|
{ prop:'id', label:'ID', width:80, fixed:true },
|
||||||
|
{ prop:'enterpriseLevel', label:'等级', width:140 },
|
||||||
|
{ prop:'scoreFrom', label:'起始分', width:100 },
|
||||||
|
{ prop:'scoreTo', label:'截止分', width:100 },
|
||||||
|
{ prop:'actions', label:'操作', width:140, slot:'actions', fixed:'right' }
|
||||||
|
]
|
||||||
|
const STATUS_DEFAULTS = [
|
||||||
|
{ value:'', label:'全部' }
|
||||||
|
]
|
||||||
|
const list = ref<LevelRow[]>([]); const total = ref(0); const loading = ref(false); const page = ref(1); const limit = ref(10); const filterKw = ref(''); const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api'|'mock'|'loading'>('loading'); const apiError = ref<string>('')
|
||||||
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet<any>(ep, { page: page.value, limit: limit.value })
|
||||||
|
const rows = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows; total.value = Number(data?.total || rows.length); dataSource.value = 'api'; apiError.value = ''; loading.value = false; return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]; total.value = FALLBACK.length; dataSource.value = 'mock'; apiError.value = '后端 API 暂未实装, 已加载前端示例数据'; loading.value = false
|
||||||
|
}
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase(); let rs = list.value
|
||||||
|
if (kw) rs = rs.filter(r => String(r.id).includes(kw) || String(r.enterpriseLevel).includes(kw))
|
||||||
|
return rs
|
||||||
|
})
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [
|
||||||
|
{ key:'id', label:'ID', type:'text' },
|
||||||
|
{ key:'enterpriseLevel', label:'等级', type:'text' }
|
||||||
|
]
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() { filterKw.value = String(filters.id || filters.enterpriseLevel || ''); page.value = 1 }
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
function onView(row: LevelRow) { ElMessage.info(`查看 ${row.id}`) }
|
||||||
|
async function onEdit(row: LevelRow) {
|
||||||
|
ElMessage.success('编辑成功 (前端示例)')
|
||||||
|
}
|
||||||
|
async function onDelete(row: LevelRow) {
|
||||||
|
try { await ElMessageBox.confirm(`确认删除等级「${row.enterpriseLevel}」?`, '提示', { type: 'warning' }) } catch { return }
|
||||||
|
ElMessage.success('已删除 (前端示例)')
|
||||||
|
}
|
||||||
|
function onExport() { ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)') }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="企业等级管理" sub="供应商等级与对应分数段配置" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_DEFAULTS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
<DataTable :columns="columns" :data="filtered"
|
||||||
|
:borderless="true" :loading="loading" :page="page" :limit="limit" :total="total" row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }" @limit-change="(l: number) => { limit = l; page = 1; load() }">
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableActions
|
||||||
|
:row="row"
|
||||||
|
:actions="[
|
||||||
|
{ label: '查看', type: 'primary', onClick: onView },
|
||||||
|
{ label: '编辑', type: 'primary', onClick: onEdit },
|
||||||
|
{ label: '删除', type: 'danger', onClick: onDelete }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
117
src/modules/user/enterpriserating/EnterpriseratingListView.vue
Normal file
117
src/modules/user/enterpriserating/EnterpriseratingListView.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 供应商评级管理 - W16
|
||||||
|
*
|
||||||
|
* 沿用老 vue user/enterprise/enterprise-rating.vue 字段
|
||||||
|
*
|
||||||
|
* 端点: 4 URL 探测 (enterpriserating/page, /list, /search, /queryPage); 404 → FALLBACK seed
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
|
interface RatingRow { id:string; enterpriseId:string; enterpriseName:string; supplierLevel:string; clientScore:number; performanceScore:number; awardScore:number; finalScore:number; deadline:string }
|
||||||
|
const ENDPOINTS = [
|
||||||
|
'/user/performancescorerecord/page',
|
||||||
|
'/user/performancescorerecord/list'
|
||||||
|
]
|
||||||
|
const FALLBACK: RatingRow[] = [
|
||||||
|
{ id:'R001', enterpriseId:'s20015', enterpriseName:'坚美建筑材料有限公司', supplierLevel:'A 战略级', clientScore:96, performanceScore:92, awardScore:5, finalScore:193, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R002', enterpriseId:'s20016', enterpriseName:'华建门窗厂', supplierLevel:'B 核心级', clientScore:88, performanceScore:85, awardScore:3, finalScore:176, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R003', enterpriseId:'s20017', enterpriseName:'融鑫管业', supplierLevel:'B 核心级', clientScore:84, performanceScore:88, awardScore:0, finalScore:172, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R004', enterpriseId:'s20018', enterpriseName:'东方五金机械', supplierLevel:'A 战略级', clientScore:92, performanceScore:90, awardScore:8, finalScore:190, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R005', enterpriseId:'s20019', enterpriseName:'美洁涂料', supplierLevel:'C 成长级', clientScore:78, performanceScore:76, awardScore:-2, finalScore:152, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R006', enterpriseId:'s20020', enterpriseName:'万家灯具', supplierLevel:'B 核心级', clientScore:86, performanceScore:82, awardScore:2, finalScore:170, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R007', enterpriseId:'s20021', enterpriseName:'顺达物流', supplierLevel:'D 入门级', clientScore:65, performanceScore:68, awardScore:0, finalScore:133, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R008', enterpriseId:'s20022', enterpriseName:'众成厨具', supplierLevel:'C 成长级', clientScore:75, performanceScore:72, awardScore:-3, finalScore:144, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R009', enterpriseId:'s20023', enterpriseName:'坚美建筑材料', supplierLevel:'A 战略级', clientScore:94, performanceScore:91, awardScore:6, finalScore:191, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R010', enterpriseId:'s20024', enterpriseName:'华建门窗厂', supplierLevel:'A 战略级', clientScore:90, performanceScore:89, awardScore:4, finalScore:183, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R011', enterpriseId:'s20025', enterpriseName:'融鑫管业', supplierLevel:'C 成长级', clientScore:74, performanceScore:78, awardScore:0, finalScore:152, deadline:'2026-06-30 23:59:59' },
|
||||||
|
{ id:'R012', enterpriseId:'s20026', enterpriseName:'美洁涂料', supplierLevel:'D 入门级', clientScore:62, performanceScore:65, awardScore:-1, finalScore:126, deadline:'2026-06-30 23:59:59' }
|
||||||
|
]
|
||||||
|
type Col = { type?: 'status'|'text'|'selection'; prop?: string; label: string; width?: number; slot?: string; fixed?: 'right'|true }
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ type:'selection', label:'', width:50 },
|
||||||
|
{ prop:'id', label:'评级ID', width:90, fixed:true },
|
||||||
|
{ prop:'enterpriseName', label:'供应商', width:200 },
|
||||||
|
{ prop:'supplierLevel', label:'等级', width:120, slot:'level' },
|
||||||
|
{ prop:'clientScore', label:'客户评价', width:100 },
|
||||||
|
{ prop:'performanceScore', label:'履约评价', width:100 },
|
||||||
|
{ prop:'awardScore', label:'奖惩分', width:90, slot:'award' },
|
||||||
|
{ prop:'finalScore', label:'最终得分', width:100, slot:'final' },
|
||||||
|
{ prop:'deadline', label:'评级时间', width:170, slot:'deadline' },
|
||||||
|
{ prop:'actions', label:'操作', width:180, slot:'actions', fixed:'right' }
|
||||||
|
]
|
||||||
|
const STATUS_DEFAULTS = [
|
||||||
|
{ value:'', label:'全部' }
|
||||||
|
]
|
||||||
|
const list = ref<RatingRow[]>([]); const total = ref(0); const loading = ref(false); const page = ref(1); const limit = ref(10); const filterKw = ref(''); const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api'|'mock'|'loading'>('loading'); const apiError = ref<string>('')
|
||||||
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet<any>(ep, { page: page.value, limit: limit.value })
|
||||||
|
const rows = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows; total.value = Number(data?.total || rows.length); dataSource.value = 'api'; apiError.value = ''; loading.value = false; return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]; total.value = FALLBACK.length; dataSource.value = 'mock'; apiError.value = '后端 API 暂未实装, 已加载前端示例数据'; loading.value = false
|
||||||
|
}
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase(); let rs = list.value
|
||||||
|
if (kw) rs = rs.filter(r => String(r.enterpriseName).includes(kw))
|
||||||
|
return rs
|
||||||
|
})
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [
|
||||||
|
{ key:'enterpriseName', label:'供应商', type:'text' }
|
||||||
|
]
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() { filterKw.value = String(filters.enterpriseName || ''); page.value = 1 }
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
function onView(row: RatingRow) { ElMessage.info(`详情 ${row.id}`) }
|
||||||
|
async function onEdit(row: RatingRow) {
|
||||||
|
ElMessage.success('已发起调整 (前端示例)')
|
||||||
|
}
|
||||||
|
function onExport() { ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)') }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="供应商评级管理" sub="供应商基于多维度打分的最终评级" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_DEFAULTS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
<DataTable :columns="columns" :data="filtered"
|
||||||
|
:borderless="true" :loading="loading" :page="page" :limit="limit" :total="total" row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }" @limit-change="(l: number) => { limit = l; page = 1; load() }">
|
||||||
|
<template #level="{ row }">
|
||||||
|
<el-tag :type="row.supplierLevel.startsWith('A') ? 'success' : row.supplierLevel.startsWith('B') ? 'primary' : row.supplierLevel.startsWith('C') ? 'warning' : 'info'" size="small">{{ row.supplierLevel }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #award="{ row }">
|
||||||
|
<span :style="{ color: row.awardScore > 0 ? '#67c23a' : row.awardScore < 0 ? '#f56c6c' : '#999' }">
|
||||||
|
{{ row.awardScore > 0 ? '+' : '' }}{{ row.awardScore }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #final="{ row }">
|
||||||
|
<span style="font-weight: 600;">{{ row.finalScore }}</span>
|
||||||
|
</template>
|
||||||
|
<template #deadline="{ row }">{{ row.deadline }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<el-button type="primary" link @click="onView(row)">详情</el-button>
|
||||||
|
<el-button type="success" link @click="onEdit(row)">调整分</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
112
src/modules/user/enterprisescore/EnterprisescoreListView.vue
Normal file
112
src/modules/user/enterprisescore/EnterprisescoreListView.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 供应商评分管理 - W16
|
||||||
|
*
|
||||||
|
* 沿用老 vue user/enterprise/enterprise-score.vue 字段
|
||||||
|
*
|
||||||
|
* 端点: 4 URL 探测 (enterprisescore/page, /list, /search, /queryPage); 404 → FALLBACK seed
|
||||||
|
*/
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
|
interface ScoreRow { id:string; enterpriseId:string; enterpriseName:string; scoreContent:string; evaluator:string; score:number; deadline:string; status:number }
|
||||||
|
const ENDPOINTS = [
|
||||||
|
'/user/performancescoredetail/page',
|
||||||
|
'/user/performancescoredetail/list'
|
||||||
|
]
|
||||||
|
const FALLBACK: ScoreRow[] = [
|
||||||
|
{ id:'SC001', enterpriseId:'s20015', enterpriseName:'坚美建筑材料有限公司', scoreContent:'商品质量', evaluator:'张工长', score:96, deadline:'2026-07-10 23:59:59', status:1 },
|
||||||
|
{ id:'SC002', enterpriseId:'s20015', enterpriseName:'坚美建筑材料有限公司', scoreContent:'配送时效', evaluator:'李采购', score:90, deadline:'2026-07-10 23:59:59', status:1 },
|
||||||
|
{ id:'SC003', enterpriseId:'s20016', enterpriseName:'华建门窗厂', scoreContent:'商品质量', evaluator:'王工长', score:88, deadline:'2026-07-10 23:59:59', status:1 },
|
||||||
|
{ id:'SC004', enterpriseId:'s20016', enterpriseName:'华建门窗厂', scoreContent:'安装服务', evaluator:'赵监理', score:92, deadline:'2026-07-10 23:59:59', status:1 },
|
||||||
|
{ id:'SC005', enterpriseId:'s20017', enterpriseName:'融鑫管业', scoreContent:'商品质量', evaluator:'钱工', score:85, deadline:'2026-07-15 23:59:59', status:0 },
|
||||||
|
{ id:'SC006', enterpriseId:'s20018', enterpriseName:'东方五金机械', scoreContent:'商品质量', evaluator:'孙经理', score:94, deadline:'2026-07-15 23:59:59', status:0 },
|
||||||
|
{ id:'SC007', enterpriseId:'s20019', enterpriseName:'美洁涂料', scoreContent:'商品质量', evaluator:'周工', score:78, deadline:'2026-07-15 23:59:59', status:0 },
|
||||||
|
{ id:'SC008', enterpriseId:'s20020', enterpriseName:'万家灯具', scoreContent:'商品质量', evaluator:'吴总', score:88, deadline:'2026-07-15 23:59:59', status:0 },
|
||||||
|
{ id:'SC009', enterpriseId:'s20021', enterpriseName:'顺达物流', scoreContent:'配送时效', evaluator:'郑采购', score:65, deadline:'2026-07-20 23:59:59', status:0 },
|
||||||
|
{ id:'SC010', enterpriseId:'s20022', enterpriseName:'众成厨具', scoreContent:'商品质量', evaluator:'冯工', score:76, deadline:'2026-07-20 23:59:59', status:0 },
|
||||||
|
{ id:'SC011', enterpriseId:'s20023', enterpriseName:'坚美建筑材料', scoreContent:'商品质量', evaluator:'陈工长', score:95, deadline:'2026-07-20 23:59:59', status:0 },
|
||||||
|
{ id:'SC012', enterpriseId:'s20024', enterpriseName:'华建门窗厂', scoreContent:'商品质量', evaluator:'褚采购', score:90, deadline:'2026-07-20 23:59:59', status:0 }
|
||||||
|
]
|
||||||
|
type Col = { type?: 'status'|'text'|'selection'; prop?: string; label: string; width?: number; slot?: string; fixed?: 'right'|true }
|
||||||
|
const columns: Col[] = [
|
||||||
|
{ type:'selection', label:'', width:50 },
|
||||||
|
{ prop:'id', label:'评分ID', width:90, fixed:true },
|
||||||
|
{ prop:'enterpriseName', label:'评分对象', width:200 },
|
||||||
|
{ prop:'scoreContent', label:'评分内容', width:140 },
|
||||||
|
{ prop:'evaluator', label:'评分员', width:120 },
|
||||||
|
{ prop:'score', label:'分值', width:90, slot:'score' },
|
||||||
|
{ prop:'status', label:'状态', width:90, slot:'status' },
|
||||||
|
{ prop:'deadline', label:'截止时间', width:170, slot:'deadline' },
|
||||||
|
{ prop:'actions', label:'操作', width:180, slot:'actions', fixed:'right' }
|
||||||
|
]
|
||||||
|
const STATUS_DEFAULTS = [
|
||||||
|
{ value:'', label:'全部' }
|
||||||
|
]
|
||||||
|
const list = ref<ScoreRow[]>([]); const total = ref(0); const loading = ref(false); const page = ref(1); const limit = ref(10); const filterKw = ref(''); const statusTab = ref('')
|
||||||
|
const dataSource = ref<'api'|'mock'|'loading'>('loading'); const apiError = ref<string>('')
|
||||||
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
for (const ep of ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet<any>(ep, { page: page.value, limit: limit.value })
|
||||||
|
const rows = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||||
|
list.value = rows; total.value = Number(data?.total || rows.length); dataSource.value = 'api'; apiError.value = ''; loading.value = false; return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
list.value = [...FALLBACK]; total.value = FALLBACK.length; dataSource.value = 'mock'; apiError.value = '后端 API 暂未实装, 已加载前端示例数据'; loading.value = false
|
||||||
|
}
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const kw = filterKw.value.trim().toLowerCase(); let rs = list.value
|
||||||
|
if (kw) rs = rs.filter(r => String(r.enterpriseName).includes(kw) || String(r.evaluator).includes(kw))
|
||||||
|
return rs
|
||||||
|
})
|
||||||
|
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [
|
||||||
|
{ key:'enterpriseName', label:'评分对象', type:'text' },
|
||||||
|
{ key:'evaluator', label:'评分员', type:'text' }
|
||||||
|
]
|
||||||
|
const filters = reactive<Record<string, any>>({})
|
||||||
|
function onSearch() { filterKw.value = String(filters.enterpriseName || filters.evaluator || ''); page.value = 1 }
|
||||||
|
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
|
||||||
|
function onView(row: ScoreRow) { ElMessage.info(`详情 ${row.id}`) }
|
||||||
|
async function onScore(row: ScoreRow) {
|
||||||
|
ElMessage.success('已跳转打分 (前端示例)')
|
||||||
|
}
|
||||||
|
function onExport() { ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)') }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="供应商评分管理" sub="供应商评分记录和评分员管理" :data-source="dataSource" :api-error="apiError">
|
||||||
|
<el-button type="primary" @click="onExport">导出明细</el-button>
|
||||||
|
</PageHeader>
|
||||||
|
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
|
||||||
|
<el-tab-pane v-for="t in STATUS_DEFAULTS" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
|
||||||
|
<DataTable :columns="columns" :data="filtered"
|
||||||
|
:borderless="true" :loading="loading" :page="page" :limit="limit" :total="total" row-key="id"
|
||||||
|
@page-change="(p: number) => { page = p; load() }" @limit-change="(l: number) => { limit = l; page = 1; load() }">
|
||||||
|
<template #score="{ row }">
|
||||||
|
<span style="font-weight: 600; color: var(--primary);">{{ row.score }}</span>
|
||||||
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'warning'" size="small">{{ row.status === 1 ? '已评分' : '未评分' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #deadline="{ row }">{{ row.deadline }}</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<el-button type="primary" link @click="onView(row)">详情</el-button>
|
||||||
|
<el-button v-if="row.status === 0" type="success" link @click="onScore(row)">去打分</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-tabs { margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
83
src/modules/user/finance/AccountTransactionListView.vue
Normal file
83
src/modules/user/finance/AccountTransactionListView.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 财务流水 - P1 真 view
|
||||||
|
*
|
||||||
|
* 走 /user/accounttransaction/page (4 条路由共用, type 区分)
|
||||||
|
* 真实接口 (2026-08-07 探测: 200, total=405)
|
||||||
|
* 字段: id/userId/accountType/type/amount/balanceAfter/payTime/payType/status/orderCode/createDate
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface Tx {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
accountType: number | null
|
||||||
|
type: number | null
|
||||||
|
amount: number
|
||||||
|
balanceAfter: number | null
|
||||||
|
payTime: string | null
|
||||||
|
payType: number | null
|
||||||
|
status: number
|
||||||
|
orderCode: string | null
|
||||||
|
createDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ type?: number }>()
|
||||||
|
|
||||||
|
const TITLES: Record<number, string> = { 1: '充值管理', 2: '提现管理', 3: '分账记录管理' }
|
||||||
|
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
||||||
|
0: { label: '待审核', type: 'warning' },
|
||||||
|
1: { label: '处理中', type: 'primary' },
|
||||||
|
2: { label: '已驳回', type: 'danger' },
|
||||||
|
3: { label: '已完成', type: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = ref<Tx[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'orderCode', label: '单号', minWidth: 180, formatter: (r: Tx) => r.orderCode || '--' },
|
||||||
|
{ prop: 'amount', label: '金额', width: 120, formatter: (r: Tx) => '¥' + Number(r.amount ?? 0).toFixed(2) },
|
||||||
|
{ prop: 'status', label: '状态', width: 110, slot: 'status' },
|
||||||
|
{ prop: 'payType', label: '支付方式', width: 110, formatter: (r: Tx) => (r.payType === 1 ? '余额' : r.payType === 2 ? '微信' : r.payType === 3 ? '支付宝' : '--') },
|
||||||
|
{ prop: 'payTime', label: '支付时间', width: 180, formatter: (r: Tx) => r.payTime || '--' },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 180 }
|
||||||
|
]
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const resp: any = await httpGet('/user/accounttransaction/page', { page: query.page, limit: query.limit, ...(props.type ? { type: props.type } : {}) })
|
||||||
|
list.value = resp?.list || []
|
||||||
|
total.value = Number(resp?.total || 0)
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader :title="TITLES[props.type ?? 0] || '交易流水'" :sub="`财务流水记录 (共 ${total} 条)`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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"
|
||||||
|
@limit-change="onLimitChange">
|
||||||
|
<template #status="{ row }">
|
||||||
|
<el-tag :type="STATUS_MAP[row.status]?.type || 'info'">{{ STATUS_MAP[row.status]?.label || row.status }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
260
src/modules/user/merchant/MerchantRegisterListView.vue
Normal file
260
src/modules/user/merchant/MerchantRegisterListView.vue
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* W18.4 企业登记管理 — 按项目 UI 规范 (PageHeader/SearchBar/DataTable) + 老 vue register.vue 功能映射
|
||||||
|
*
|
||||||
|
* 后端端点: ApiEnterpriseController @ /user/enterprise
|
||||||
|
* GET /user/enterprise/page 分页 (EnterpriseDTO 含 merchantStatus/merchantNo/terminalNo)
|
||||||
|
* GET /user/enterprise/merchant/info 商户进件信息 (EnterpriseMerchantDTO 全字段)
|
||||||
|
* GET /user/enterprise/merchant/status 商户进件状态 (同 DTO)
|
||||||
|
* POST /user/enterprise/merchant/register/submit 提交进件
|
||||||
|
* POST /user/enterprise/merchant/elec-contract/apply 生成电子合同
|
||||||
|
*
|
||||||
|
* EnterpriseMerchantDTO 真实字段 (来源: devbox 反编译确认, 2026-08-06):
|
||||||
|
* enterpriseId/userNo/lakalaMerchantNo/cupMerchantNo/termNo/registerStatus
|
||||||
|
* registerOrderNo/auditFailReason/ecOrderNo/ecApplyId/ecResultUrl/ecNo
|
||||||
|
* ecStatus/ecUpdateDate/shopId/addTermStatus/addTermReviewId/addTermFailReason
|
||||||
|
* email/busiCode/merchantType/merchantName/merchantAddress
|
||||||
|
* provinceCode/provinceName/cityCode/cityName/countyCode/countyName
|
||||||
|
* licenseNo/licenseName/licenseStartDate/licenseEndDate
|
||||||
|
* longitude/latitude/legalPersonName/legalPersonIdCard/legalPersonIdType
|
||||||
|
* legalPersonIdCardStart/legalPersonIdCardEnd
|
||||||
|
* contactMobile/contactName/openningBankCode/openningBankName/clearingBankCode
|
||||||
|
* settleProvinceCode/settleProvinceName/settleCityCode/settleCityName
|
||||||
|
* accountNo/accountName/accountType/accountIdCard/settleType/settlementType
|
||||||
|
* source/businessContent/idCardFrontUrl/idCardBackUrl/businessLicenseUrl/openingPermitUrl
|
||||||
|
*
|
||||||
|
* 状态映射 (老 vue register.vue setStatus):
|
||||||
|
* registerStatus: 1=审核中 2=审核通过 3=审核失败, 其他=未进件
|
||||||
|
* ecStatus: UNDONE=待签署 COMPLETED=已签署
|
||||||
|
* merchantStatus (EnterpriseDTO): 0=未绑定 1=审核中 2=进件中 3=有效 4=无效
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
|
||||||
|
interface Enterprise {
|
||||||
|
id: string
|
||||||
|
enterpriseName: string
|
||||||
|
merchantNo: string | null
|
||||||
|
terminalNo: string | null
|
||||||
|
merchantStatus: number | null
|
||||||
|
administrator: string
|
||||||
|
administratorMobile: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = ref<Enterprise[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
const limit = ref(10)
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
enterpriseName: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// merchantStatus 映射 (EnterpriseDTO: 0=未绑定 1=审核中 2=进件中 3=有效 4=无效)
|
||||||
|
const MERCHANT_STATUS_MAP: Record<number, { label: string; cls: string }> = {
|
||||||
|
0: { label: '未绑定', cls: 'rr-tag-info' },
|
||||||
|
1: { label: '审核中', cls: 'rr-tag-warn' },
|
||||||
|
2: { label: '进件中', cls: 'rr-tag-warn' },
|
||||||
|
3: { label: '有效', cls: 'rr-tag-ok' },
|
||||||
|
4: { label: '无效', cls: 'rr-tag-err' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerStatus 映射 (老 vue: 1=审核中 2=审核通过 3=审核失败, 其他=未进件)
|
||||||
|
const REGISTER_STATUS_MAP: Record<number, { label: string; cls: string }> = {
|
||||||
|
1: { label: '审核中', cls: 'rr-tag-warn' },
|
||||||
|
2: { label: '审核通过', cls: 'rr-tag-ok' },
|
||||||
|
3: { label: '审核失败', cls: 'rr-tag-err' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskMobile(mobile?: string | null) {
|
||||||
|
if (!mobile) return ''
|
||||||
|
return mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: Record<string, any> = { page: page.value, limit: limit.value }
|
||||||
|
if (filters.enterpriseName) params.enterpriseName = filters.enterpriseName
|
||||||
|
|
||||||
|
const data = await httpGet<{ total: number; list: any[] }>('/user/enterprise/page', params)
|
||||||
|
list.value = (data.list || []).map((it: any) => ({
|
||||||
|
id: String(it.id),
|
||||||
|
enterpriseName: it.enterpriseName || '--',
|
||||||
|
merchantNo: it.merchantNo || null,
|
||||||
|
terminalNo: it.terminalNo || null,
|
||||||
|
merchantStatus: it.merchantStatus ?? null,
|
||||||
|
administrator: it.administrator || '--',
|
||||||
|
administratorMobile: it.administratorMobile || ''
|
||||||
|
}))
|
||||||
|
total.value = data.total || 0
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '加载企业列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() { page.value = 1; load() }
|
||||||
|
function onReset() { filters.enterpriseName = ''; page.value = 1; load() }
|
||||||
|
function onPageChange(p: number) { page.value = p; load() }
|
||||||
|
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||||
|
|
||||||
|
// ========== 进件详情弹窗 ==========
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
|
const merchant = ref<Record<string, any> | null>(null)
|
||||||
|
|
||||||
|
async function showMerchantDetail(row: Enterprise) {
|
||||||
|
detailVisible.value = true
|
||||||
|
detailLoading.value = true
|
||||||
|
merchant.value = null
|
||||||
|
try {
|
||||||
|
const data = await httpGet<any>('/user/enterprise/merchant/info', { enterpriseId: row.id })
|
||||||
|
merchant.value = data || {}
|
||||||
|
} catch (e: any) {
|
||||||
|
merchant.value = null
|
||||||
|
ElMessage.warning(e?.msg || e?.message || '该企业暂无进件信息')
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function regStatus() {
|
||||||
|
const s = merchant.value?.registerStatus
|
||||||
|
return REGISTER_STATUS_MAP[s as number]?.label || '未进件'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ecStatus() {
|
||||||
|
const s = merchant.value?.ecStatus
|
||||||
|
if (s === 'COMPLETED') return '已签署'
|
||||||
|
if (s === 'UNDONE') return '待签署'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => load())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader title="企业登记管理" :sub="`共 ${total} 家企业`" />
|
||||||
|
|
||||||
|
<SearchBar
|
||||||
|
v-model:filters="filters"
|
||||||
|
:fields="[
|
||||||
|
{ key: 'enterpriseName', label: '企业名称', type: 'text', placeholder: '企业名称' }
|
||||||
|
]"
|
||||||
|
@search="onSearch"
|
||||||
|
@reset="onReset"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="rr-list-card">
|
||||||
|
<DataTable
|
||||||
|
:columns="[
|
||||||
|
{ label: '企业名称', minWidth: 220, slot: 'enterpriseName' },
|
||||||
|
{ label: '管理员/手机号', minWidth: 160, slot: 'adminInfo' },
|
||||||
|
{ prop: 'merchantNo', label: '商户号', width: 160, align: 'center' },
|
||||||
|
{ prop: 'terminalNo', label: '终端号', width: 160, align: 'center' },
|
||||||
|
{ label: '商户状态', width: 110, slot: 'merchantStatus', align: 'center' },
|
||||||
|
{ label: '操作', width: 120, slot: 'action', fixed: 'right', align: 'center' }
|
||||||
|
]"
|
||||||
|
:data="list"
|
||||||
|
:loading="loading"
|
||||||
|
:total="total"
|
||||||
|
:page="page"
|
||||||
|
:limit="limit"
|
||||||
|
:borderless="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
row-key="id"
|
||||||
|
empty-text="暂无企业"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
@limit-change="onLimitChange"
|
||||||
|
>
|
||||||
|
<template #enterpriseName="{ row }">
|
||||||
|
<span style="font-weight:500">{{ row.enterpriseName }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #adminInfo="{ row }">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:2px">
|
||||||
|
<span>{{ row.administrator }}</span>
|
||||||
|
<span style="font-size:12px;color:var(--ink-3)">{{ maskMobile(row.administratorMobile) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #merchantStatus="{ row }">
|
||||||
|
<span v-if="MERCHANT_STATUS_MAP[row.merchantStatus as number]"
|
||||||
|
:class="['rr-status-tag', MERCHANT_STATUS_MAP[row.merchantStatus as number].cls]">
|
||||||
|
{{ MERCHANT_STATUS_MAP[row.merchantStatus as number].label }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="rr-muted">--</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-button type="primary" size="small" text @click="showMerchantDetail(row)">进件详情</el-button>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进件详情弹窗 (EnterpriseMerchantDTO 核心字段) -->
|
||||||
|
<el-dialog v-model="detailVisible" title="商户进件详情" width="760px" :close-on-click-modal="false" destroy-on-close>
|
||||||
|
<div v-loading="detailLoading">
|
||||||
|
<template v-if="merchant">
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px">
|
||||||
|
<span class="rr-status-tag" :class="REGISTER_STATUS_MAP[merchant.registerStatus as number]?.cls || 'rr-tag-info'">
|
||||||
|
进件状态: {{ regStatus() }}
|
||||||
|
</span>
|
||||||
|
<span class="rr-status-tag rr-tag-ok" v-if="merchant.ecStatus === 'COMPLETED'">电子合同: 已签署</span>
|
||||||
|
<span class="rr-status-tag rr-tag-warn" v-else-if="merchant.ecStatus === 'UNDONE'">电子合同: 待签署</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border size="small">
|
||||||
|
<el-descriptions-item label="商户名称">{{ merchant.merchantName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商户类型">{{ merchant.merchantType || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="拉卡拉商户号">{{ merchant.lakalaMerchantNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="银联商户号">{{ merchant.cupMerchantNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="终端号">{{ merchant.termNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="进件单号">{{ merchant.registerOrderNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商户地址" :span="2">{{ merchant.merchantAddress || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系人">{{ merchant.contactName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系人手机">{{ maskMobile(merchant.contactMobile) || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人代表">{{ merchant.legalPersonName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="经营内容">{{ merchant.businessContent || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="营业执照名称">{{ merchant.licenseName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="营业执照号">{{ merchant.licenseNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结算账户名称">{{ merchant.accountName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结算账户号">{{ merchant.accountNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结算账户类型">{{ merchant.accountType || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="电子合同号">{{ merchant.ecNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="电子合同订单号">{{ merchant.ecOrderNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item v-if="merchant.registerStatus === 3" label="审核失败原因" :span="2" style="color:#c62828">
|
||||||
|
{{ merchant.auditFailReason || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
<el-empty v-else-if="!detailLoading" description="该企业暂无进件信息" :image-size="80" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailVisible=false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { padding: 16px; }
|
||||||
|
.rr-list-card {
|
||||||
|
background: var(--surface); border-radius: 12px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.rr-status-tag {
|
||||||
|
display: inline-block; padding: 2px 10px; border-radius: 4px; font-size: 12px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.rr-tag-info { background: #e3f2fd; color: #1565c0; }
|
||||||
|
.rr-tag-warn { background: #fff3e0; color: #e65100; }
|
||||||
|
.rr-tag-ok { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.rr-tag-err { background: #fce4ec; color: #c62828; }
|
||||||
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
||||||
|
</style>
|
||||||
72
src/modules/user/message/MessageListView.vue
Normal file
72
src/modules/user/message/MessageListView.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F006 系统消息 - P1 真 view
|
||||||
|
*
|
||||||
|
* 走 /user/message/page
|
||||||
|
* 真实接口 (2026-08-07 探测: 200, total=112)
|
||||||
|
* 字段: id/type/pushType/name/content/readFlag/itemCode/createDate
|
||||||
|
*/
|
||||||
|
import { onMounted, ref, reactive } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
|
interface Msg {
|
||||||
|
id: string
|
||||||
|
type: number
|
||||||
|
pushType: number | null
|
||||||
|
name: string
|
||||||
|
content: string
|
||||||
|
readFlag: number
|
||||||
|
itemCode: string | null
|
||||||
|
createDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ status?: number }>()
|
||||||
|
const list = ref<Msg[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
|
||||||
|
const READ_MAP: Record<number, { label: string; type: string }> = { 0: { label: '未读', type: 'danger' }, 1: { label: '已读', type: 'success' } }
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 90 },
|
||||||
|
{ prop: 'name', label: '消息名称', minWidth: 180 },
|
||||||
|
{ prop: 'content', label: '内容', minWidth: 280 },
|
||||||
|
{ prop: 'readFlag', label: '状态', width: 100, slot: 'read' },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 180 }
|
||||||
|
]
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const resp: any = await httpGet('/user/message/page', { page: query.page, limit: query.limit, ...(props.status !== undefined ? { status: props.status } : {}) })
|
||||||
|
list.value = resp?.list || []
|
||||||
|
total.value = Number(resp?.total || 0)
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rr-page">
|
||||||
|
<PageHeader :title="props.status === 0 ? '待处理消息' : '系统消息'" :sub="`${props.status === 0 ? '待处理' : '站内'}消息 (共 ${total} 条)`" />
|
||||||
|
<div class="rr-card">
|
||||||
|
<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"
|
||||||
|
@limit-change="onLimitChange">
|
||||||
|
<template #read="{ row }">
|
||||||
|
<el-tag :type="READ_MAP[row.readFlag]?.type || 'info'">{{ READ_MAP[row.readFlag]?.label || row.readFlag }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.rr-page { max-width: 1400px; }
|
||||||
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
|
</style>
|
||||||
@ -87,6 +87,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/modules/user/EnterpriseListView.vue'),
|
component: () => import('@/modules/user/EnterpriseListView.vue'),
|
||||||
meta: { title: '企业管理' }
|
meta: { title: '企业管理' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/user/enterprise/merchant/register',
|
||||||
|
name: 'MerchantRegisterList',
|
||||||
|
component: () => import('@/modules/user/merchant/MerchantRegisterListView.vue'),
|
||||||
|
meta: { title: '企业登记管理' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/user/finance',
|
path: '/user/finance',
|
||||||
name: 'UserFinanceList',
|
name: 'UserFinanceList',
|
||||||
@ -174,10 +180,9 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { title: '服务商品' }
|
meta: { title: '服务商品' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// W3.x C P2 + W17.3.7: 商品店铺后端未实现, redirect 到 supplier/good 实物商品页
|
||||||
path: '/product/shop',
|
path: '/product/shop',
|
||||||
name: 'ProductShop',
|
redirect: '/supplier/good'
|
||||||
component: () => import('@/modules/product/shop/ShopListView.vue'),
|
|
||||||
meta: { title: '店铺管理' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/message/feedback',
|
path: '/message/feedback',
|
||||||
@ -194,19 +199,19 @@ const routes: RouteRecordRaw[] = [
|
|||||||
|
|
||||||
// ============ W5.x.x+4: 50 个 P3 路由 - MockTableView 集中配置 ============
|
// ============ W5.x.x+4: 50 个 P3 路由 - MockTableView 集中配置 ============
|
||||||
// 4 个 message
|
// 4 个 message
|
||||||
{ path: '/message/sms', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'message-sms' }), meta: { title: '短信配置' } },
|
{ path: '/message/sms', component: () => import('@/modules/message/sms/SmsListView.vue'), meta: { title: '短信配置' } },
|
||||||
{ path: '/message/sms-log', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'message-sms-log' }), meta: { title: '短信日志' } },
|
{ path: '/message/sms-log', component: () => import('@/modules/message/smslog/SmsLogListView.vue'), meta: { title: '短信日志' } },
|
||||||
{ path: '/message/mail-template', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'message-mail-template' }), meta: { title: '邮件模板' } },
|
{ path: '/message/mail-template', component: () => import('@/modules/message/mailtemplate/MailTemplateListView.vue'), meta: { title: '邮件模板' } },
|
||||||
{ path: '/message/mail-log', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'message-mail-log' }), meta: { title: '邮件日志' } },
|
{ path: '/message/mail-log', component: () => import('@/modules/message/maillog/MailLogListView.vue'), meta: { title: '邮件日志' } },
|
||||||
// 4 个运营中心
|
// 4 个运营中心 (P1: mock -> 真 view, /sys/operation/pageList type 区分)
|
||||||
{ path: '/sys/operation', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-operation-country' }), meta: { title: '全国运营中心' } },
|
{ path: '/sys/operation', component: () => import('@/modules/sys/operation/OperationListView.vue'), props: () => ({ type: 1 }), meta: { title: '全国运营中心' } },
|
||||||
{ path: '/sys/operation/province', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-operation-province' }), meta: { title: '省级运营中心' } },
|
{ path: '/sys/operation/province', component: () => import('@/modules/sys/operation/OperationListView.vue'), props: () => ({ type: 2 }), meta: { title: '省级运营中心' } },
|
||||||
{ path: '/sys/operation/county', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-operation-county' }), meta: { title: '县级运营中心' } },
|
{ path: '/sys/operation/county', component: () => import('@/modules/sys/operation/OperationListView.vue'), props: () => ({ type: 3 }), meta: { title: '县级运营中心' } },
|
||||||
{ path: '/sys/operation/association', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-operation-association' }), meta: { title: '行业协会运营' } },
|
{ path: '/sys/operation/association', component: () => import('@/modules/sys/operation/OperationListView.vue'), props: () => ({ type: 4 }), meta: { title: '行业协会运营' } },
|
||||||
// 3 个资讯类
|
// 3 个资讯类 (P1: information -> 真 view, industry/categoryZX 404 保留 mock)
|
||||||
{ path: '/other/information', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-information' }), meta: { title: '资讯列表' } },
|
{ path: '/other/information', component: () => import('@/modules/other/information/InformationListView.vue'), meta: { title: '资讯列表' } },
|
||||||
{ path: '/other/industry', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-industry' }), meta: { title: '行业' } },
|
{ path: '/other/industry', component: () => import('@/modules/other/category/CategoryDeepListView.vue'), props: { type: 1, title: '行业管理' }, meta: { title: '行业' } },
|
||||||
{ path: '/other/category/zx', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-category-zx' }), meta: { title: '资讯分类' } },
|
{ path: '/other/category/zx', component: () => import('@/modules/other/category/CategoryDeepListView.vue'), props: { type: 3, title: '资讯分类' }, meta: { title: '资讯分类' } },
|
||||||
// devtools 5 个
|
// devtools 5 个
|
||||||
{ path: '/devtools/baseclass', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'devtools-baseclass' }), meta: { title: '基类配置' } },
|
{ path: '/devtools/baseclass', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'devtools-baseclass' }), meta: { title: '基类配置' } },
|
||||||
{ path: '/devtools/datasource', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'devtools-datasource' }), meta: { title: '数据源配置' } },
|
{ path: '/devtools/datasource', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'devtools-datasource' }), meta: { title: '数据源配置' } },
|
||||||
@ -214,7 +219,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{ path: '/devtools/param', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'devtools-param' }), meta: { title: '参数配置(开发)' } },
|
{ path: '/devtools/param', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'devtools-param' }), meta: { title: '参数配置(开发)' } },
|
||||||
{ path: '/devtools/template', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'devtools-template' }), meta: { title: '模板配置' } },
|
{ path: '/devtools/template', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'devtools-template' }), meta: { title: '模板配置' } },
|
||||||
// job 1 个
|
// job 1 个
|
||||||
{ path: '/job/schedule', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'job-schedule' }), meta: { title: '定时任务' } },
|
{ path: '/job/schedule', component: () => import('@/modules/sys/ScheduleListView.vue'), meta: { title: '定时任务' } },
|
||||||
// order 9 个
|
// order 9 个
|
||||||
{ path: '/order/ordergood', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品订单' } },
|
{ path: '/order/ordergood', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品订单' } },
|
||||||
{ path: '/order/serviceorder', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务订单' } },
|
{ path: '/order/serviceorder', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务订单' } },
|
||||||
@ -223,38 +228,54 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{ path: '/order/ordergood/commission', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '实物订单分佣统计' } },
|
{ path: '/order/ordergood/commission', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '实物订单分佣统计' } },
|
||||||
{ path: '/order/serviceorder/commission', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务订单分佣统计' } },
|
{ path: '/order/serviceorder/commission', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务订单分佣统计' } },
|
||||||
{ path: '/order/ordergood/after', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品售后订单' } },
|
{ path: '/order/ordergood/after', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品售后订单' } },
|
||||||
|
{ path: '/order/serviceorder/after', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务售后订单' } },
|
||||||
{ path: '/order/orderserviceafter', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务售后订单' } },
|
{ path: '/order/orderserviceafter', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务售后订单' } },
|
||||||
{ path: '/order/base-price-analysis', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '基础价格分析表' } },
|
{ path: '/order/base-price-analysis', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '基础价格分析表' } },
|
||||||
|
// W15 新增 - 订单售后 + 退款 + 付款回调 (前端 max-effort mock, 老 vue 同字段)
|
||||||
|
{ path: '/order/orderpay', component: () => import('@/modules/order/payment/PaymentCallbackListView.vue'), meta: { title: '订单支付回调' } },
|
||||||
|
{ path: '/order/refund', component: () => import('@/modules/order/refund/RefundProcessListView.vue'), meta: { title: '订单退款管理' } },
|
||||||
|
{ path: '/order/aftersale', component: () => import('@/modules/order/aftersale/AftersaleListView.vue'), meta: { title: '售后订单' } },
|
||||||
// other 7 个
|
// other 7 个
|
||||||
{ path: '/other/brand', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-brand' }), meta: { title: '品牌管理' } },
|
{ path: '/other/brand', component: () => import('@/modules/other/supplierBrand/SupplierBrandListView.vue'), meta: { title: '品牌管理' } },
|
||||||
{ path: '/other/category/good', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-category-good' }), meta: { title: '平台分类' } },
|
// W18 真 view 切换 (原 MockTableView -> 真页面)
|
||||||
{ path: '/other/help', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-help' }), meta: { title: '帮助中心' } },
|
{ path: '/other/category/good', component: () => import('@/modules/other/categoryGood/CategoryGoodListView.vue'), meta: { title: '平台分类' } },
|
||||||
{ path: '/other/guide', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-guide' }), meta: { title: '内容列表' } },
|
{ path: '/other/help', component: () => import('@/modules/other/help/HelpListView.vue'), meta: { title: '帮助中心' } },
|
||||||
{ path: '/other/textconfig', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-textconfig' }), meta: { title: '常量配置' } },
|
{ path: '/other/guide', component: () => import('@/modules/other/operateguide/OperateguideListView.vue'), meta: { title: '内容列表' } },
|
||||||
{ path: '/other/groupbuy', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-groupbuy' }), meta: { title: '拼团活动' } },
|
{ path: '/other/textconfig', component: () => import('@/modules/other/textconfig/TextconfigListView.vue'), meta: { title: '常量配置' } },
|
||||||
{ path: '/other/seckill', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-seckill' }), meta: { title: '秒杀活动' } },
|
{ path: '/other/groupbuy', component: () => import('@/modules/other/groupbuy/GroupbuyListView.vue'), meta: { title: '拼团活动' } },
|
||||||
|
{ path: '/other/seckill', component: () => import('@/modules/other/seckill/SeckillListView.vue'), meta: { title: '秒杀活动' } },
|
||||||
// supplier 2 个
|
// supplier 2 个
|
||||||
{ path: '/supplier/coupon', component: () => import('@/modules/supplier/coupon/CouponListView.vue'), meta: { title: '优惠券' } }, // W7: 独立 view, 走 /supplier/coupon/page 真 API
|
{ path: '/supplier/coupon', component: () => import('@/modules/supplier/coupon/CouponListView.vue'), meta: { title: '优惠券' } }, // W7: 独立 view, 走 /supplier/coupon/page 真 API
|
||||||
{ path: '/supplier/promoter', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'supplier-promoter' }), meta: { title: '推广返佣' } },
|
{ path: '/supplier/promoter', component: () => import('@/modules/supplier/promoter/PromoterListView.vue'), meta: { title: '推广员管理' } },
|
||||||
|
{ path: '/supplier/historylog', component: () => import('@/modules/supplier/historylog/HistorylogListView.vue'), meta: { title: '浏览足迹' } },
|
||||||
|
{ path: '/supplier/ordercomment', component: () => import('@/modules/supplier/ordercomment/OrderCommentListView.vue'), meta: { title: '订单评价' } },
|
||||||
|
{ path: '/supplier/logistics', component: () => import('@/modules/supplier/logistics/LogisticsListView.vue'), meta: { title: '物流方案' } },
|
||||||
|
{ path: '/supplier/coupon-scope', component: () => import('@/modules/supplier/couponscope/CouponScopeListView.vue'), meta: { title: '优惠券适用范围' } },
|
||||||
// sys 11 个
|
// sys 11 个
|
||||||
{ path: '/sys/dict', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-dict' }), meta: { title: '字典类型' } },
|
{ path: '/sys/dict', component: () => import('@/modules/sys/DictTypeListView.vue'), meta: { title: '字典类型' } },
|
||||||
{ path: '/sys/log/login', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-log-login' }), meta: { title: '登录日志' } },
|
{ path: '/sys/log/login', component: () => import('@/modules/sys/LogLoginView.vue'), meta: { title: '登录日志' } },
|
||||||
{ path: '/sys/log/operation', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-log-operation' }), meta: { title: '操作日志' } },
|
{ path: '/sys/log/operation', component: () => import('@/modules/sys/LogOperationView.vue'), meta: { title: '操作日志' } },
|
||||||
{ path: '/sys/log/error', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-log-error' }), meta: { title: '异常日志' } },
|
{ path: '/sys/log/error', component: () => import('@/modules/sys/LogErrorView.vue'), meta: { title: '异常日志' } },
|
||||||
{ path: '/sys/notice', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-notice' }), meta: { title: '通知公告' } },
|
{ path: '/sys/notice', component: () => import('@/modules/sys/NoticeListView.vue'), meta: { title: '通知公告' } },
|
||||||
{ path: '/sys/params', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-params' }), meta: { title: '参数配置' } },
|
{ path: '/sys/params', component: () => import('@/modules/sys/ParamsListView.vue'), meta: { title: '参数配置' } },
|
||||||
{ path: '/sys/notice-user', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-notice-user' }), meta: { title: '用户通知' } },
|
{ path: '/sys/notice-user', component: () => import('@/modules/sys/noticeuser/NoticeUserListView.vue'), meta: { title: '用户通知' } },
|
||||||
{ path: '/sys/userManagement', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-userManagement' }), meta: { title: '全国运营中心' } },
|
{ path: '/sys/userManagement', redirect: '/sys/operation', meta: { title: '全国运营中心' } },
|
||||||
|
// W18 后台用户管理 5 个 (老 vue sys/user 模块, 真 view 已存在)
|
||||||
|
{ path: '/sys/user', component: () => import('@/modules/sys/UserListView.vue'), meta: { title: '用户管理' } },
|
||||||
|
{ path: '/sys/role', component: () => import('@/modules/sys/RoleListView.vue'), meta: { title: '角色管理' } },
|
||||||
|
{ path: '/sys/post', component: () => import('@/modules/sys/PostListView.vue'), meta: { title: '岗位管理' } },
|
||||||
|
{ path: '/sys/dept', component: () => import('@/modules/sys/DeptListView.vue'), meta: { title: '部门管理' } },
|
||||||
|
{ path: '/sys/menu', component: () => import('@/modules/sys/MenuListView.vue'), meta: { title: '菜单管理' } },
|
||||||
// user 9 个
|
// user 9 个
|
||||||
{ path: '/user/enterprise/level', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-enterprise-level' }), meta: { title: '企业等级管理' } },
|
{ path: '/user/enterprise/level', component: () => import('@/modules/user/enterpriselevel/EnterpriselevelListView.vue'), meta: { title: '企业等级管理' } },
|
||||||
{ path: '/user/enterprise/rating', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-enterprise-rating' }), meta: { title: '评级' } },
|
{ path: '/user/enterprise/rating', component: () => import('@/modules/user/enterpriserating/EnterpriseratingListView.vue'), meta: { title: '供应商评级' } },
|
||||||
{ path: '/user/enterprise/score', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-enterprise-score' }), meta: { title: '评分' } },
|
{ path: '/user/enterprise/score', component: () => import('@/modules/user/enterprisescore/EnterprisescoreListView.vue'), meta: { title: '供应商评分' } },
|
||||||
{ path: '/user/finance/recharge', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-finance-recharge' }), meta: { title: '充值管理' } },
|
{ path: '/user/finance/recharge', component: () => import('@/modules/user/finance/AccountTransactionListView.vue'), props: () => ({ type: 1 }), meta: { title: '充值管理' } },
|
||||||
{ path: '/user/finance/recharge-tx', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-finance-recharge-tx' }), meta: { title: '提现管理' } },
|
{ path: '/user/finance/recharge-tx', component: () => import('@/modules/user/finance/AccountTransactionListView.vue'), props: () => ({ type: 2 }), meta: { title: '提现管理' } },
|
||||||
{ path: '/user/finance/revenue', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-finance-revenue' }), meta: { title: '分账记录管理' } },
|
{ path: '/user/finance/revenue', component: () => import('@/modules/user/finance/AccountTransactionListView.vue'), props: () => ({ type: 3 }), meta: { title: '分账记录管理' } },
|
||||||
{ path: '/user/finance/transaction', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-finance-transaction' }), meta: { title: '交易流水' } },
|
{ path: '/user/finance/transaction', component: () => import('@/modules/user/finance/AccountTransactionListView.vue'), meta: { title: '交易流水' } },
|
||||||
{ path: '/user/message', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-message' }), meta: { title: '系统消息' } },
|
{ path: '/user/message', component: () => import('@/modules/user/message/MessageListView.vue'), meta: { title: '系统消息' } },
|
||||||
{ path: '/user/message/handel', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'user-message-handel' }), meta: { title: '待处理消息' } },
|
{ path: '/user/message/handel', component: () => import('@/modules/user/message/MessageListView.vue'), props: { status: 0 }, meta: { title: '待处理消息' } },
|
||||||
|
|
||||||
// W3.1 通用通配占位
|
// W3.1 通用通配占位
|
||||||
{
|
{
|
||||||
|
|||||||
@ -69,10 +69,12 @@ export const useMenuStore = defineStore('menu', () => {
|
|||||||
if (!filtered.length) return null
|
if (!filtered.length) return null
|
||||||
return { ...item, children: filtered }
|
return { ...item, children: filtered }
|
||||||
}
|
}
|
||||||
// 子菜单: url 必须在 MENU_LABELS 里, 且首次出现
|
// F002-W18 修: 后端 url 可能带 / 前缀 (如 /user/enterprise/enterprise),
|
||||||
if (!allowedUrls.has(item.url)) return null
|
// MENU_LABELS 不带 / (如 user/enterprise/enterprise), 规范化后匹配
|
||||||
if (seenUrls.has(item.url)) return null
|
const normalizedUrl = item.url.replace(/^\//, '')
|
||||||
seenUrls.add(item.url)
|
if (!allowedUrls.has(item.url) && !allowedUrls.has(normalizedUrl)) return null
|
||||||
|
if (seenUrls.has(normalizedUrl)) return null
|
||||||
|
seenUrls.add(normalizedUrl)
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
.filter((item): item is ResolvedMenu => item !== null)
|
.filter((item): item is ResolvedMenu => item !== null)
|
||||||
|
|||||||
123
src/styles/global-popper.scss
Normal file
123
src/styles/global-popper.scss
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
/* === 全局弹层圆角 (按 ui-preview-v2.html 模板: --radius 8px) ===
|
||||||
|
EP 弹层 (select-dropdown / popper / dropdown / menu) 通过 teleport 到 body,
|
||||||
|
不在组件 scope 内, 必须全局生效。双重保险:① 改 EP CSS 变量根 ② 写 !important selector */
|
||||||
|
|
||||||
|
/* === 1) 改 EP CSS 变量根 (从源头覆盖, 所有用 var(--el-*) 的弹层都生效) === */
|
||||||
|
:root {
|
||||||
|
--el-border-radius-base: 8px;
|
||||||
|
--el-border-radius-small: 6px;
|
||||||
|
--el-popper-border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 2) 显式 selector + !important (覆盖任何内联样式) === */
|
||||||
|
.el-select-dropdown,
|
||||||
|
.el-select__popper,
|
||||||
|
.el-select-v2__popper,
|
||||||
|
.el-dropdown-menu,
|
||||||
|
.el-dropdown__popper,
|
||||||
|
.el-menu--popup,
|
||||||
|
.el-popper,
|
||||||
|
.el-popper.is-light,
|
||||||
|
.el-picker__popper,
|
||||||
|
.el-autocomplete__popper,
|
||||||
|
.el-autocomplete-suggestion,
|
||||||
|
.el-cascader__dropdown,
|
||||||
|
.el-cascader-panel,
|
||||||
|
.el-color-picker__panel,
|
||||||
|
.el-tooltip__popper,
|
||||||
|
.el-message-box,
|
||||||
|
.el-notification,
|
||||||
|
.el-drawer {
|
||||||
|
border-radius: var(--radius, 8px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹层内容滚动层 (Element Plus 弹层有 .el-popper__content 内部容器) */
|
||||||
|
.el-popper .el-popper__content,
|
||||||
|
.el-select-dropdown .el-scrollbar {
|
||||||
|
border-radius: var(--radius, 8px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 3) dropdown 项 hover/选中态 (跟模版统一: brand-soft 高亮 + brand 蓝字) === */
|
||||||
|
.el-select-dropdown__item,
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
border-radius: var(--radius-sm, 6px) !important;
|
||||||
|
margin: 2px 4px !important;
|
||||||
|
padding: 0 12px !important;
|
||||||
|
height: 34px !important;
|
||||||
|
line-height: 34px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
color: var(--ink-2, #334155);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.el-select-dropdown__item.is-hovering,
|
||||||
|
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||||
|
background: var(--brand-soft, rgba(37, 99, 235, 0.08)) !important;
|
||||||
|
color: var(--brand, #2563EB) !important;
|
||||||
|
}
|
||||||
|
.el-select-dropdown__item.is-selected {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: var(--brand, #2563EB) !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 4) 弹层背景跟主题 (避免 dark 下弹层仍是浅色 + 文字看不清) === */
|
||||||
|
.el-select-dropdown,
|
||||||
|
.el-dropdown-menu,
|
||||||
|
.el-popper.is-light,
|
||||||
|
.el-cascader__dropdown,
|
||||||
|
.el-color-picker__panel {
|
||||||
|
background: var(--surface, #FFFFFF) !important;
|
||||||
|
border: 1px solid var(--border, #E2E8F0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 5) input 圆角: 跟模版一致 (按 ui-preview-v2.html: 1px 描边 + 8px 圆角 + 34px 高) === */
|
||||||
|
.el-input__wrapper,
|
||||||
|
.el-select__wrapper,
|
||||||
|
.el-textarea__inner {
|
||||||
|
border-radius: var(--radius, 8px) !important;
|
||||||
|
box-shadow: 0 0 0 1px var(--border, #E2E8F0) inset !important;
|
||||||
|
min-height: 34px;
|
||||||
|
transition: box-shadow 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.el-input__wrapper:hover,
|
||||||
|
.el-select__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px var(--ink-3, #94A3B8) inset !important;
|
||||||
|
}
|
||||||
|
.el-input__wrapper.is-focus,
|
||||||
|
.el-select__wrapper.is-focused {
|
||||||
|
box-shadow: 0 0 0 1px var(--brand, #2563EB) inset, 0 0 0 3px var(--brand-glow, rgba(37, 99, 235, 0.18)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === W18.9 全局滚动条暗主题适配 (深色滚动条避免割裂) === */
|
||||||
|
/* 默认 (亮主题): 浅灰滚动条 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border, #E2E8F0);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--ink-4, #94A3B8);
|
||||||
|
}
|
||||||
|
/* Firefox */
|
||||||
|
html {
|
||||||
|
scrollbar-color: var(--border, #E2E8F0) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗主题: 深色滚动条适配, 避免浅白滚动条在深色背景上刺眼 */
|
||||||
|
[data-theme="dark"] ::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border, #475569);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--ink-3, #CBD5E1);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] html {
|
||||||
|
scrollbar-color: var(--border, #475569) transparent;
|
||||||
|
}
|
||||||
@ -191,4 +191,39 @@
|
|||||||
--el-color-primary-light-8: var(--brand-soft);
|
--el-color-primary-light-8: var(--brand-soft);
|
||||||
--el-color-primary-light-9: var(--brand-soft);
|
--el-color-primary-light-9: var(--brand-soft);
|
||||||
--el-color-primary-dark-2: var(--brand-hover);
|
--el-color-primary-dark-2: var(--brand-hover);
|
||||||
|
|
||||||
|
// W18 dark 主题适配:EP 内部 SCSS 常量重写
|
||||||
|
// :root 块里 var(--xxx) 解析时, dark 块的重写还未生效, 必须用实际值硬编码
|
||||||
|
--el-color-white: #1E293B; /* surface */
|
||||||
|
--el-color-black: #F8FAFC; /* ink */
|
||||||
|
--el-text-color-primary: #F8FAFC; /* ink */
|
||||||
|
--el-text-color-regular: #E2E8F0; /* ink-2 */
|
||||||
|
--el-text-color-secondary: #CBD5E1; /* ink-3 */
|
||||||
|
--el-text-color-placeholder: #94A3B8; /* ink-4 */
|
||||||
|
--el-text-color-disabled: #94A3B8; /* ink-4 */
|
||||||
|
--el-border-color: #475569; /* border */
|
||||||
|
--el-border-color-light: #475569;
|
||||||
|
--el-border-color-lighter: #475569;
|
||||||
|
--el-border-color-extra-light: #475569;
|
||||||
|
--el-border-color-dark: #64748B; /* border-strong */
|
||||||
|
--el-border-color-darker: #64748B;
|
||||||
|
--el-fill-color: #0F172A; /* bg-soft */
|
||||||
|
--el-fill-color-light: #0F172A;
|
||||||
|
--el-fill-color-lighter: #283449; /* surface-2 */
|
||||||
|
--el-fill-color-extra-light: #283449;
|
||||||
|
--el-fill-color-dark: #283449;
|
||||||
|
--el-fill-color-darker: #283449;
|
||||||
|
--el-fill-color-blank: #1E293B; /* surface */
|
||||||
|
--el-bg-color: #1E293B;
|
||||||
|
--el-bg-color-page: #0F172A; /* bg-soft */
|
||||||
|
--el-bg-color-overlay: #1E293B;
|
||||||
|
--el-table-tr-bg-color: #1E293B; /* surface */
|
||||||
|
--el-table-bg-color: #1E293B;
|
||||||
|
--el-table-expanded-cell-bg-color: #1E293B;
|
||||||
|
--el-table-header-bg-color: #283449; /* surface-2 (表头略亮区分) */
|
||||||
|
--el-table-row-hover-bg-color: #0F172A; /* bg-soft */
|
||||||
|
--el-tooltip-bg-color: #1E293B; /* surface */
|
||||||
|
--el-disabled-bg-color: #0F172A; /* bg-soft */
|
||||||
|
--el-disabled-text-color: #94A3B8; /* ink-4 */
|
||||||
|
--el-disabled-border-color: #475569;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
// ===== 圆角(沿用 v1.3) =====
|
// ===== 圆角(沿用 v1.3) =====
|
||||||
--radius-sm: 6px;
|
--radius-sm: 6px;
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
--radius-lg: 12px;
|
--radius-lg: 10px;
|
||||||
--radius-xl: 16px;
|
--radius-xl: 16px;
|
||||||
--radius-pill: 9999px;
|
--radius-pill: 9999px;
|
||||||
|
|
||||||
@ -120,4 +120,12 @@
|
|||||||
|
|
||||||
// ===== v2 新增:主按钮渐变(从主题 brand 自动组合) =====
|
// ===== v2 新增:主按钮渐变(从主题 brand 自动组合) =====
|
||||||
--brand-gradient: linear-gradient(135deg, var(--brand), var(--brand-2));
|
--brand-gradient: linear-gradient(135deg, var(--brand), var(--brand-2));
|
||||||
|
|
||||||
|
// ===== W18 暗主题适配:补齐 EP 缺失 token (dark 下表格背景/边框自适应) =====
|
||||||
|
// EP 内部用 --el-color-white / black, 未重定向, dark 下仍是 #FFFFFF
|
||||||
|
--el-color-white: var(--surface);
|
||||||
|
--el-color-black: var(--ink);
|
||||||
|
// el-table 表格体/展开行背景
|
||||||
|
--el-table-tr-bg-color: var(--surface);
|
||||||
|
--el-table-expanded-cell-bg-color: var(--surface);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,9 @@ export interface ApiEnvelope<T = any> {
|
|||||||
const STORAGE_KEY = 'access_token'
|
const STORAGE_KEY = 'access_token'
|
||||||
|
|
||||||
export const http: AxiosInstance = axios.create({
|
export const http: AxiosInstance = axios.create({
|
||||||
baseURL: '',
|
// F002-W18 修: baseURL '/api' 让巨变数字 nginx /api/ location 转发 (去 /api 前缀)
|
||||||
|
// devbox nginx 同样有 /api/ location (rewrite ^/api/(.*)$ /$1 break), 兼容
|
||||||
|
baseURL: '/api',
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -39,12 +41,13 @@ http.interceptors.response.use(
|
|||||||
const body = response.data as ApiEnvelope | undefined
|
const body = response.data as ApiEnvelope | undefined
|
||||||
if (body && typeof body === 'object' && 'code' in body) {
|
if (body && typeof body === 'object' && 'code' in body) {
|
||||||
if (body.code === 0) return response
|
if (body.code === 0) return response
|
||||||
// 业务错:401 token 过期 / 其它业务错
|
// 业务级 code:401
|
||||||
if (body.code === 401 || body.code === 401000) {
|
// 未登录(无 token)访问受保护接口(如 /sys/menu/nav)→ 跳登录页
|
||||||
|
// 已登录但权限不足(如 /openapi/** 带 token 被 Spring Security 拒)→ 保持 reject 给调用方
|
||||||
|
if (body.code === 401 && !localStorage.getItem(STORAGE_KEY)) {
|
||||||
handleAuthExpired()
|
handleAuthExpired()
|
||||||
return Promise.reject(body)
|
return Promise.reject(body)
|
||||||
}
|
}
|
||||||
// 业务错(不弹 toast,留给调用方决定)
|
|
||||||
return Promise.reject(body)
|
return Promise.reject(body)
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
|
|||||||
178
src/views/theme/ThemeConfigView.vue
Normal file
178
src/views/theme/ThemeConfigView.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<!--
|
||||||
|
* F006 主题配置 (管理后台)
|
||||||
|
* - 选 4 套预设主题之一,应用到 pc-前端-v2 全站
|
||||||
|
* - 后端接口:
|
||||||
|
* GET /sys/openapi/theme/current → { theme: "industrial-blue", available: [...] }
|
||||||
|
* POST /sys/openapi/theme/update?theme=xxx 切换主题
|
||||||
|
* - 切换后 pc-前端-v2 下次刷新或主页轮询时会拿到最新主题
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
|
|
||||||
|
interface Theme {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
primary: string
|
||||||
|
'primary-container': string
|
||||||
|
secondary: string
|
||||||
|
surface: string
|
||||||
|
'text-primary': string
|
||||||
|
border: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const themes = ref<Theme[]>([
|
||||||
|
{
|
||||||
|
key: 'industrial-blue',
|
||||||
|
label: '工业蓝',
|
||||||
|
description: '默认主题,适用于工业品 B2B 大宗交易场景',
|
||||||
|
primary: '#0B5394', 'primary-container': '#E8F0FA',
|
||||||
|
secondary: '#E07B00', surface: '#FFFFFF',
|
||||||
|
'text-primary': '#1A1F2C', border: '#E2E6EE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'steel-red',
|
||||||
|
label: '钢红',
|
||||||
|
description: '国企/政策导向客户首选,庄重正式',
|
||||||
|
primary: '#A8202E', 'primary-container': '#FAE8EB',
|
||||||
|
secondary: '#C99B30', surface: '#FFFFFF',
|
||||||
|
'text-primary': '#1A1F2C', border: '#EFE2E4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'forest-green',
|
||||||
|
label: '森林绿',
|
||||||
|
description: '能源/碳中和/化工场景,传递可持续发展',
|
||||||
|
primary: '#1F6E4D', 'primary-container': '#E5F2EC',
|
||||||
|
secondary: '#D4A017', surface: '#FFFFFF',
|
||||||
|
'text-primary': '#1A1F2C', border: '#E2EAE5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'midnight-slate',
|
||||||
|
label: '暗夜深灰',
|
||||||
|
description: '数据型暗模式,适合长时间盯盘的运营人员',
|
||||||
|
primary: '#3B82F6', 'primary-container': '#1E293B',
|
||||||
|
secondary: '#60A5FA', surface: '#0F172A',
|
||||||
|
'text-primary': '#F1F5F9', border: '#334155',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const current = ref<string>('')
|
||||||
|
const preview = ref<string>('') // 用户选中但未保存
|
||||||
|
const saving = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const updateTime = ref('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await httpGet('/sys/openapi/theme/current')
|
||||||
|
if (res.code === 0) {
|
||||||
|
current.value = res.data.theme
|
||||||
|
preview.value = res.data.theme
|
||||||
|
updateTime.value = new Date().toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载当前主题失败: ' + (e as Error).message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!preview.value) return
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await httpPost(`/sys/openapi/theme/update?theme=${preview.value}`)
|
||||||
|
if (res.code === 0) {
|
||||||
|
current.value = preview.value
|
||||||
|
updateTime.value = new Date().toLocaleString('zh-CN')
|
||||||
|
ElMessage.success(`主题已切换为「${findByKey(preview.value)?.label}」,pc-前端-v2 下次访问将自动应用`)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error('保存失败: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findByKey(key: string) {
|
||||||
|
return themes.value.find(t => t.key === key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirty = computed(() => preview.value !== current.value)
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader title="主题配置" :sub="`当前生效主题 · ${findByKey(current)?.label || '加载中'} · 更新于 ${updateTime}`">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<el-tag v-if="dirty" type="warning" effect="plain">有未保存的更改</el-tag>
|
||||||
|
<el-button type="primary" :disabled="!dirty || saving" :loading="saving" @click="save">
|
||||||
|
保存并应用
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="grid grid-cols-1 lg:grid-cols-2 gap-5 mt-5">
|
||||||
|
<div
|
||||||
|
v-for="theme in themes"
|
||||||
|
:key="theme.key"
|
||||||
|
class="rounded-2xl border-2 transition-all cursor-pointer overflow-hidden hover:-translate-y-1 hover:shadow-xl"
|
||||||
|
:class="preview === theme.key
|
||||||
|
? 'border-primary shadow-lg shadow-primary/20'
|
||||||
|
: 'border-border'"
|
||||||
|
@click="preview = theme.key"
|
||||||
|
>
|
||||||
|
<!-- 预览头 -->
|
||||||
|
<div
|
||||||
|
class="h-24 flex items-center justify-between px-5 text-white"
|
||||||
|
:style="{ background: `linear-gradient(135deg, ${theme.primary}, ${theme.secondary})` }"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="text-lg font-bold">{{ theme.label }}</div>
|
||||||
|
<div class="text-xs opacity-80 mt-1">{{ theme.key }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-icon v-if="preview === theme.key" :size="22"><Check /></el-icon>
|
||||||
|
<el-tag v-if="current === theme.key" type="success" effect="dark" size="small">当前生效</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview mock -->
|
||||||
|
<div class="p-5 space-y-3" :style="{ backgroundColor: theme.surface, color: theme['text-primary'] }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold"
|
||||||
|
:style="{ backgroundColor: theme.primary }">M</div>
|
||||||
|
<div class="font-bold text-sm">LOGO 主品牌色</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-70">{{ theme.description }}</div>
|
||||||
|
<div class="flex gap-2 mt-3">
|
||||||
|
<button class="px-4 py-1.5 rounded-lg text-xs font-bold text-white"
|
||||||
|
:style="{ backgroundColor: theme.primary }">主按钮</button>
|
||||||
|
<button class="px-4 py-1.5 rounded-lg text-xs font-bold border-2"
|
||||||
|
:style="{ borderColor: theme.primary, color: theme.primary }">描边按钮</button>
|
||||||
|
<span class="px-3 py-1 rounded text-xs font-bold"
|
||||||
|
:style="{ backgroundColor: theme['primary-container'], color: theme.primary }">标签</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] opacity-50 pt-2 border-t" :style="{ borderColor: theme.border }">
|
||||||
|
HEX: {{ theme.primary }} · {{ theme.secondary }} · {{ theme['text-primary'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
class="mt-6"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
title="关于主题切换"
|
||||||
|
description="点击主题卡片预览,保存后 pc-前端-v2 的所有页面(包括其他用户正在浏览)将在下次请求 / 重载时生效。无需重启服务。"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue
Block a user