F006 W3.xC + W5.x.x+1/3/4/5 + W6 + W7 完整批次 (17 commits) #1
@ -1,81 +1,174 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 意见反馈 - W3.x C P2
|
||||
* 后端 /api/feedback/page 端点状态: 404 / 400 (端点不存在或参数错)
|
||||
* 走 el-result 优雅占位 + 展示参考老 /vue 后台的预期字段
|
||||
* F006 意见反馈 - W6 P0 完整化
|
||||
*
|
||||
* 沿用老 vue api/feedback.vue 137 行:
|
||||
* - 搜索: 用户昵称
|
||||
* - 表格列: userId/nickName/phone/title/content/orderCode/createDate
|
||||
* - 操作: 预览 / 删除
|
||||
* - 预览弹窗展示完整反馈内容 (含图片列表)
|
||||
* - /api/feedback 后端 404 → FALLBACK mock
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
interface FieldSpec { key: string; label: string; desc: string }
|
||||
interface Feedback {
|
||||
id: string
|
||||
userId: string
|
||||
nickName: string
|
||||
phone: string
|
||||
title: string
|
||||
content: string
|
||||
orderCode: string
|
||||
createDate: string
|
||||
imgList: string[]
|
||||
raw: any
|
||||
}
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
|
||||
const expectedFields: FieldSpec[] = [
|
||||
{ key: 'title', label: '标题', desc: '反馈标题' },
|
||||
{ key: 'content', label: '反馈内容', desc: '详细描述' },
|
||||
{ key: 'orderCode', label: '订单编号', desc: '关联订单 (可空)' },
|
||||
{ key: 'createDate', label: '创建时间', desc: '提交时间' },
|
||||
{ key: 'nickName', label: '用户昵称', desc: '提交人' },
|
||||
{ key: 'phone', label: '电话', desc: '联系电话' },
|
||||
{ key: 'imgList', label: '反馈图片', desc: '图片数组 (预览)' }
|
||||
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: '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: 'F003', userId: 'U10088', nickName: '王采购', phone: '13700137003', title: '系统使用咨询', content: '请问如何批量导出订单? 需要按月统计采购金额。', orderCode: '', createDate: '2026-07-02 14:08:00', imgList: [], raw: {} },
|
||||
{ id: 'F004', userId: 'U10089', nickName: '赵总', phone: '13600136004', title: '希望增加新的支付方式', content: '目前只支持对公转账, 希望增加企业网银在线支付。', orderCode: '', createDate: '2026-07-03 10:22:11', imgList: [], raw: {} },
|
||||
{ id: 'F005', userId: 'U10090', nickName: '钱运营', phone: '13500135005', title: '订单退款申请', content: '订单已取消但款项未到账, 请核实。订单号见附件。', orderCode: 'O20260701003', createDate: '2026-07-03 16:45:30', imgList: ['https://placehold.co/120x120/F59E0B/fff?text=REFUND'], raw: {} },
|
||||
{ id: 'F006', userId: 'U10091', nickName: '孙技术', phone: '13400134006', title: '功能建议: 自定义审批流', content: '建议在询价单模块增加自定义审批流, 当前固定流程不够灵活。', orderCode: '', createDate: '2026-07-04 09:10:00', imgList: [], raw: {} }
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
const list = ref<Feedback[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(20)
|
||||
const filters = reactive({ nickName: '' })
|
||||
|
||||
const previewVisible = ref(false)
|
||||
const previewData = ref<Feedback | null>(null)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const r: any = await httpGet('/api/feedback/page', { page: 1, limit: 1 })
|
||||
if (r?.code === 0) status.value = '200'
|
||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
if (filters.nickName) rows = rows.filter(r => r.nickName.includes(filters.nickName))
|
||||
list.value = rows
|
||||
total.value = rows.length
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
||||
status.value = '404'
|
||||
errorMsg.value = '后端 API 未实现 (/api/feedback/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'
|
||||
errorMsg.value = e?.msg || e?.message || '请求失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = FALLBACK
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.nickName = ''; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; load() }
|
||||
function onPreview(row: Feedback) { previewData.value = row; previewVisible.value = true }
|
||||
function onDelete(row: Feedback) {
|
||||
ElMessageBox.confirm(`确认删除「${row.title}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<div>
|
||||
<h2 class="rr-page-title">意见反馈</h2>
|
||||
<p class="rr-page-sub">用户反馈信息 - 数据接口未实现, 占位页展示预期字段</p>
|
||||
<p class="rr-page-sub">用户反馈信息管理 (共 {{ total }} 条)</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 || '请联系后端实现 /api/feedback/page 接口')"
|
||||
</div>
|
||||
|
||||
<div class="rr-card rr-search">
|
||||
<el-form :inline="true" @keyup.enter="onSearch">
|
||||
<el-form-item label="用户昵称">
|
||||
<el-input v-model="filters.nickName" placeholder="请输入用户昵称" clearable style="width: 240px" />
|
||||
</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>
|
||||
|
||||
<div class="rr-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'userId', label: '用户ID', width: 120 },
|
||||
{ prop: 'nickName', label: '用户昵称', width: 140 },
|
||||
{ prop: 'phone', label: '电话', width: 140 },
|
||||
{ prop: 'title', label: '标题', minWidth: 200 },
|
||||
{ prop: 'content', label: '反馈内容', minWidth: 280 },
|
||||
{ prop: 'orderCode', label: '订单编号', width: 160 },
|
||||
{ prop: 'createDate', label: '创建时间', width: 180 },
|
||||
{ label: '操作', width: 160, align: 'center', slot: 'action' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="total > 10"
|
||||
:error="error"
|
||||
empty-text="暂无反馈"
|
||||
borderless
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<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 #action="{ row }">
|
||||
<el-button text type="primary" size="small" @click="onPreview(row)">预览</el-button>
|
||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</DataTable>
|
||||
</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>
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
<el-dialog v-model="previewVisible" title="反馈内容" width="700px">
|
||||
<el-form label-width="100px" v-if="previewData">
|
||||
<el-form-item label="用户昵称">
|
||||
<el-input :model-value="previewData.nickName" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话">
|
||||
<el-input :model-value="previewData.phone" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="标题">
|
||||
<el-input :model-value="previewData.title" type="textarea" :rows="2" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容">
|
||||
<el-input :model-value="previewData.content" type="textarea" :rows="6" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单编号">
|
||||
<el-input :model-value="previewData.orderCode || '--'" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="反馈图片" v-if="previewData.imgList && previewData.imgList.length">
|
||||
<div class="feedback-imgs">
|
||||
<el-image v-for="(img, idx) in previewData.imgList" :key="idx" :src="img" :preview-src-list="previewData.imgList" :initial-index="idx" fit="cover" class="feedback-img-item" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="previewVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</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); }
|
||||
.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-img-item { width: 80px; height: 80px; border-radius: 6px; border: 1px solid var(--border); }
|
||||
</style>
|
||||
|
||||
@ -1,56 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 帮助中心 - W3.x C P0.6
|
||||
* 后端 /api/help/* 暂未实现,展示检测状态
|
||||
* F006 帮助中心 - W6 P0 完整化
|
||||
*
|
||||
* 沿用老 vue api/help.vue 60 行:
|
||||
* - 搜索: 标题
|
||||
* - 表格列: title/content/sort/createDate
|
||||
* - 操作: 修改 / 删除
|
||||
* - /api/help 后端 404 → FALLBACK mock
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
interface Help {
|
||||
id: string
|
||||
title: string
|
||||
content: string
|
||||
sort: number
|
||||
createDate: string
|
||||
raw: any
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const FALLBACK: Help[] = [
|
||||
{ id: 'H001', title: '如何注册账号', content: '在登录页点击"立即注册"按钮, 填写企业信息即可注册账号。', sort: 100, createDate: '2026-06-15 10:00:00', raw: {} },
|
||||
{ id: 'H002', title: '如何发布询价', content: '进入询价管理, 点击"发布询价", 填写商品信息、采购数量、期望价格后提交。', sort: 95, createDate: '2026-06-15 10:01:00', raw: {} },
|
||||
{ id: 'H003', title: '支付方式说明', content: '平台支持对公转账、企业网银、平台余额三种支付方式。', sort: 90, createDate: '2026-06-20 14:30:00', raw: {} },
|
||||
{ id: 'H004', title: '物流配送范围', content: '平台物流覆盖全国 (除港澳台), 大宗商品支持自提。', sort: 85, createDate: '2026-06-22 09:15:00', raw: {} },
|
||||
{ id: 'H005', title: '退款规则', content: '未发货订单可全额退款; 已发货订单需双方协商。', sort: 80, createDate: '2026-06-25 11:00:00', raw: {} },
|
||||
{ id: 'H006', title: '发票申请流程', content: '订单完成后, 在订单详情页点击"申请发票", 填写开票信息。', sort: 75, createDate: '2026-06-28 16:20:00', raw: {} },
|
||||
{ id: 'H007', title: '投诉与建议', content: '可在"意见反馈"模块提交, 我们会在 24 小时内回复。', sort: 70, createDate: '2026-07-01 09:00:00', raw: {} }
|
||||
]
|
||||
|
||||
const list = ref<Help[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(20)
|
||||
const filters = reactive({ title: '' })
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editMode = ref<'add' | 'edit'>('add')
|
||||
const editForm = reactive<Help>({ id: '', title: '', content: '', sort: 0, createDate: '', raw: {} })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const r: any = await httpGet('/api/help/page', { page: 1, limit: 1 })
|
||||
if (r?.code === 0) status.value = '200'
|
||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
if (filters.title) rows = rows.filter(r => r.title.includes(filters.title))
|
||||
list.value = rows
|
||||
total.value = rows.length
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
||||
status.value = '404'; errorMsg.value = '后端 API 未实现(/api/help/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'; errorMsg.value = e?.message || '请求失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = FALLBACK
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.title = ''; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; load() }
|
||||
function onAdd() { editMode.value = 'add'; editForm.id = ''; editForm.title = ''; editForm.content = ''; editForm.sort = 0; editForm.createDate = ''; editVisible.value = true }
|
||||
function onEdit(row: Help) { editMode.value = 'edit'; Object.assign(editForm, JSON.parse(JSON.stringify(row))); editVisible.value = true }
|
||||
function onSubmitEdit() {
|
||||
if (!editForm.title.trim()) { ElMessage.warning('请填写标题'); return }
|
||||
ElMessage.success(editMode.value === 'add' ? '新增成功 (mock)' : '修改成功 (mock)')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
function onDelete(row: Help) {
|
||||
ElMessageBox.confirm(`确认删除「${row.title}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<div>
|
||||
<h2 class="rr-page-title">帮助中心</h2>
|
||||
<p class="rr-page-sub">使用指南 + FAQ 列表</p>
|
||||
<p class="rr-page-sub">常见问题与帮助文档管理 (共 {{ total }} 条)</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 || '请联系后端实现 /api/help/page 接口')"
|
||||
<div class="rr-page-actions">
|
||||
<el-button type="primary" @click="onAdd">新增文档</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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: 280px" />
|
||||
</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>
|
||||
|
||||
<div class="rr-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'title', label: '标题', minWidth: 200 },
|
||||
{ prop: 'content', label: '内容', minWidth: 360 },
|
||||
{ prop: 'sort', label: '排序', width: 100, align: 'center' },
|
||||
{ prop: 'createDate', label: '创建时间', width: 180 },
|
||||
{ label: '操作', width: 160, align: 'center', slot: 'action' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="total > 10"
|
||||
:error="error"
|
||||
empty-text="暂无帮助文档"
|
||||
borderless
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<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 #action="{ row }">
|
||||
<el-button text type="primary" size="small" @click="onEdit(row)">修改</el-button>
|
||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<!-- 增改弹窗 -->
|
||||
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增帮助' : '修改帮助'" width="560px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="标题" required>
|
||||
<el-input v-model="editForm.title" placeholder="请输入标题" maxlength="50" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" required>
|
||||
<el-input v-model="editForm.content" type="textarea" :rows="6" placeholder="请输入内容" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="editForm.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</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: 40px 20px; }
|
||||
.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>
|
||||
|
||||
@ -1,56 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 轮播管理 - W3.x C P0.5
|
||||
* 后端 /api/lunbo/* 暂未实现,展示检测状态
|
||||
* F006 轮播图管理 - W6 P0 完整化
|
||||
*
|
||||
* 沿用老 vue api/lunbo.vue 88 行:
|
||||
* - 搜索: 标题 / 展示位置
|
||||
* - 表格列: title/type(1图文/2url/3政策/4商品/5无)/image(80px缩略)/position(1首页/2商城)/sort/createDate
|
||||
* - 操作: 修改 / 删除
|
||||
* - /api/lunbo 后端 404 → FALLBACK mock
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
interface Lunbo {
|
||||
id: string
|
||||
title: string
|
||||
type: number
|
||||
image: string
|
||||
position: number
|
||||
sort: number
|
||||
createDate: string
|
||||
raw: any
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const TYPE_MAP: Record<number, { label: string; type: string }> = {
|
||||
1: { label: '图文', type: 'primary' },
|
||||
2: { label: 'URL', type: 'success' },
|
||||
3: { label: '政策公告', type: 'warning' },
|
||||
4: { label: '商品详情', type: 'info' },
|
||||
5: { label: '无跳转', type: '' }
|
||||
}
|
||||
|
||||
const POS_MAP: Record<number, { label: string; type: string }> = {
|
||||
1: { label: '首页', type: 'primary' },
|
||||
2: { label: '在线商城', type: 'success' }
|
||||
}
|
||||
|
||||
const FALLBACK: Lunbo[] = [
|
||||
{ id: 'L001', title: '7月采购节主推', type: 1, image: 'https://placehold.co/120x80/2563EB/fff?text=Banner+1', position: 1, sort: 100, createDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: 'L002', title: '政策公告-新规上线', type: 3, image: 'https://placehold.co/120x80/DC2626/fff?text=Policy', position: 1, sort: 95, createDate: '2026-07-02 09:00:00', raw: {} },
|
||||
{ id: 'L003', title: '商城精选商品', type: 4, image: 'https://placehold.co/120x80/5645D4/fff?text=Shop', position: 2, sort: 90, createDate: '2026-07-02 14:00:00', raw: {} },
|
||||
{ id: 'L004', title: '钢材特惠专场', type: 2, image: 'https://placehold.co/120x80/059669/fff?text=Steel', position: 1, sort: 88, createDate: '2026-07-03 11:20:00', raw: {} },
|
||||
{ id: 'L005', title: '企业入驻指南', type: 5, image: 'https://placehold.co/120x80/F59E0B/fff?text=Guide', position: 2, sort: 80, createDate: '2026-07-03 16:00:00', raw: {} },
|
||||
{ id: 'L006', title: '建材采购季', type: 1, image: 'https://placehold.co/120x80/8B5CF6/fff?text=Build', position: 1, sort: 75, createDate: '2026-07-04 10:30:00', raw: {} }
|
||||
]
|
||||
|
||||
const list = ref<Lunbo[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(20)
|
||||
const filters = reactive({ title: '', position: '' })
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editMode = ref<'add' | 'edit'>('add')
|
||||
const editForm = reactive<Lunbo>({ id: '', title: '', type: 1, image: '', position: 1, sort: 0, createDate: '', raw: {} })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const r: any = await httpGet('/api/lunbo/page', { page: 1, limit: 1 })
|
||||
if (r?.code === 0) status.value = '200'
|
||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
if (filters.title) rows = rows.filter(r => r.title.includes(filters.title))
|
||||
if (filters.position) rows = rows.filter(r => r.position === Number(filters.position))
|
||||
list.value = rows
|
||||
total.value = rows.length
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
||||
status.value = '404'; errorMsg.value = '后端 API 未实现(/api/lunbo/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'; errorMsg.value = e?.message || '请求失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = FALLBACK
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.title = ''; filters.position = ''; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; load() }
|
||||
function onAdd() { editMode.value = 'add'; editForm.id = ''; editForm.title = ''; editForm.type = 1; editForm.image = ''; editForm.position = 1; editForm.sort = 0; editForm.createDate = ''; editVisible.value = true }
|
||||
function onEdit(row: Lunbo) { editMode.value = 'edit'; Object.assign(editForm, JSON.parse(JSON.stringify(row))); editVisible.value = true }
|
||||
function onSubmitEdit() {
|
||||
if (!editForm.title.trim()) { ElMessage.warning('请填写标题'); return }
|
||||
if (!editForm.image.trim()) { ElMessage.warning('请填写图片URL'); return }
|
||||
ElMessage.success(editMode.value === 'add' ? '新增成功 (mock)' : '修改成功 (mock)')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
function onDelete(row: Lunbo) {
|
||||
ElMessageBox.confirm(`确认删除「${row.title}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">轮播管理</h2>
|
||||
<p class="rr-page-sub">Banner 轮播图配置</p>
|
||||
<div>
|
||||
<h2 class="rr-page-title">轮播图管理</h2>
|
||||
<p class="rr-page-sub">App/小程序首页与商城轮播图配置 (共 {{ total }} 张)</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 || '请联系后端实现 /api/lunbo/page 接口')"
|
||||
<div class="rr-page-actions">
|
||||
<el-button type="primary" @click="onAdd">新增轮播</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="展示位置">
|
||||
<el-select v-model="filters.position" placeholder="全部" clearable style="width: 160px">
|
||||
<el-option label="首页" :value="1" />
|
||||
<el-option label="在线商城" :value="2" />
|
||||
</el-select>
|
||||
</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>
|
||||
|
||||
<div class="rr-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'title', label: '标题', minWidth: 200 },
|
||||
{ prop: 'type', label: '类型', width: 110, align: 'center', slot: 'type' },
|
||||
{ prop: 'image', label: '图片', width: 140, align: 'center', slot: 'image' },
|
||||
{ prop: 'position', label: '展示位置', width: 120, align: 'center', slot: 'position' },
|
||||
{ prop: 'sort', label: '排序', width: 90, align: 'center' },
|
||||
{ prop: 'createDate', label: '创建时间', width: 180 },
|
||||
{ label: '操作', width: 160, align: 'center', slot: 'action' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="total > 10"
|
||||
:error="error"
|
||||
empty-text="暂无轮播图"
|
||||
borderless
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<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 #type="{ row }">
|
||||
<el-tag :type="(TYPE_MAP[row.type]?.type as any)" size="small" effect="light">{{ TYPE_MAP[row.type]?.label || `类型${row.type}` }}</el-tag>
|
||||
</template>
|
||||
</el-result>
|
||||
<template #image="{ row }">
|
||||
<el-image :src="row.image" fit="cover" class="lunbo-thumb" :preview-src-list="[row.image]" />
|
||||
</template>
|
||||
<template #position="{ row }">
|
||||
<el-tag :type="(POS_MAP[row.position]?.type as any)" size="small" effect="plain">{{ POS_MAP[row.position]?.label || `位置${row.position}` }}</el-tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<el-button text type="primary" size="small" @click="onEdit(row)">修改</el-button>
|
||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增轮播' : '修改轮播'" width="560px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="标题" required>
|
||||
<el-input v-model="editForm.title" placeholder="请输入标题" maxlength="30" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" required>
|
||||
<el-select v-model="editForm.type" style="width: 100%">
|
||||
<el-option label="图文" :value="1" />
|
||||
<el-option label="URL" :value="2" />
|
||||
<el-option label="政策公告" :value="3" />
|
||||
<el-option label="商品详情" :value="4" />
|
||||
<el-option label="无跳转" :value="5" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片URL" required>
|
||||
<el-input v-model="editForm.image" placeholder="请输入图片地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="展示位置" required>
|
||||
<el-radio-group v-model="editForm.position">
|
||||
<el-radio :value="1">首页</el-radio>
|
||||
<el-radio :value="2">在线商城</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="editForm.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</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: 40px 20px; }
|
||||
.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); }
|
||||
</style>
|
||||
|
||||
@ -1,58 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 平台分类 (categoryGood) - W3.x C P1
|
||||
* 后端端点 404 占位
|
||||
* F006 商品分类 - W6 P0 完整化
|
||||
*
|
||||
* 沿用老 vue other/categoryGood.vue 273 行:
|
||||
* - 搜索: 分类名
|
||||
* - 树形表格 (3 级分类), 含 level 缩进 + 1 级加粗
|
||||
* - 表格列: name(树形) / createDate
|
||||
* - 操作: 上移 / 下移 / 新增子分类 / 修改 / 删除
|
||||
* - 批量删除
|
||||
* - /other/categoryGood 后端 404 → FALLBACK mock
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
interface Category {
|
||||
id: string
|
||||
pid: string
|
||||
name: string
|
||||
level: number
|
||||
sort: number
|
||||
createDate: string
|
||||
children?: Category[]
|
||||
raw: any
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const r: any = await httpGet('/other/categoryGood/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 未实现 (/other/categoryGood/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'
|
||||
errorMsg.value = e?.message || '请求失败'
|
||||
const TREE_DATA: Category[] = [
|
||||
{ id: 'C001', pid: '0', name: '钢材', level: 1, sort: 1, createDate: '2026-06-01 10:00:00', raw: {}, children: [
|
||||
{ id: 'C001-1', pid: 'C001', name: '螺纹钢', level: 2, sort: 1, createDate: '2026-06-01 10:00:00', raw: {}, children: [
|
||||
{ id: 'C001-1-1', pid: 'C001-1', name: 'HRB400 12mm', level: 3, sort: 1, createDate: '2026-06-01 10:00:00', raw: {} },
|
||||
{ id: 'C001-1-2', pid: 'C001-1', name: 'HRB400 16mm', level: 3, sort: 2, createDate: '2026-06-01 10:00:00', raw: {} },
|
||||
{ id: 'C001-1-3', pid: 'C001-1', name: 'HRB500 20mm', level: 3, sort: 3, createDate: '2026-06-01 10:00:00', raw: {} }
|
||||
] },
|
||||
{ id: 'C001-2', pid: 'C001', name: '盘螺', level: 2, sort: 2, createDate: '2026-06-01 10:00:00', raw: {}, children: [
|
||||
{ id: 'C001-2-1', pid: 'C001-2', name: '8mm 盘螺', level: 3, sort: 1, createDate: '2026-06-01 10:00:00', raw: {} },
|
||||
{ id: 'C001-2-2', pid: 'C001-2', name: '10mm 盘螺', level: 3, sort: 2, createDate: '2026-06-01 10:00:00', raw: {} }
|
||||
] },
|
||||
{ id: 'C001-3', pid: 'C001', name: '热轧钢板', level: 2, sort: 3, createDate: '2026-06-01 10:00:00', raw: {} }
|
||||
] },
|
||||
{ id: 'C002', pid: '0', name: '水泥', level: 1, sort: 2, createDate: '2026-06-01 10:00:00', raw: {}, children: [
|
||||
{ id: 'C002-1', pid: 'C002', name: '普通硅酸盐水泥', level: 2, sort: 1, createDate: '2026-06-01 10:00:00', raw: {}, children: [
|
||||
{ id: 'C002-1-1', pid: 'C002-1', name: 'P.O 42.5', level: 3, sort: 1, createDate: '2026-06-01 10:00:00', raw: {} },
|
||||
{ id: 'C002-1-2', pid: 'C002-1', name: 'P.O 52.5', level: 3, sort: 2, createDate: '2026-06-01 10:00:00', raw: {} }
|
||||
] },
|
||||
{ id: 'C002-2', pid: 'C002', name: '矿渣硅酸盐水泥', level: 2, sort: 2, createDate: '2026-06-01 10:00:00', raw: {} }
|
||||
] },
|
||||
{ id: 'C003', pid: '0', name: '砂石', level: 1, sort: 3, createDate: '2026-06-01 10:00:00', raw: {}, children: [
|
||||
{ id: 'C003-1', pid: 'C003', name: '机制砂', level: 2, sort: 1, createDate: '2026-06-01 10:00:00', raw: {} },
|
||||
{ id: 'C003-2', pid: 'C003', name: '河砂', level: 2, sort: 2, createDate: '2026-06-01 10:00:00', raw: {} }
|
||||
] },
|
||||
{ id: 'C004', pid: '0', name: '五金工具', level: 1, sort: 4, createDate: '2026-06-01 10:00:00', raw: {} }
|
||||
]
|
||||
|
||||
const tree = ref<Category[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const filters = reactive({ name: '' })
|
||||
const selection = ref<Category[]>([])
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editMode = ref<'add' | 'edit'>('add')
|
||||
const editForm = reactive<Category>({ id: '', pid: '0', name: '', level: 1, sort: 0, createDate: '', raw: {} })
|
||||
|
||||
const filteredTree = computed(() => {
|
||||
if (!filters.name) return tree.value
|
||||
function walk(items: Category[]): Category[] {
|
||||
const result: Category[] = []
|
||||
for (const item of items) {
|
||||
if (item.name.includes(filters.name)) { result.push({ ...item }); continue }
|
||||
if (item.children) {
|
||||
const sub = walk(item.children)
|
||||
if (sub.length) result.push({ ...item, children: sub })
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return walk(tree.value)
|
||||
})
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
tree.value = TREE_DATA
|
||||
} catch (e: any) {
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
tree.value = TREE_DATA
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() {}
|
||||
function onReset() { filters.name = '' }
|
||||
function onAdd(parent?: Category) {
|
||||
editMode.value = 'add'
|
||||
const lvl = parent ? parent.level + 1 : 1
|
||||
editForm.id = ''
|
||||
editForm.pid = parent?.id || '0'
|
||||
editForm.name = ''
|
||||
editForm.level = Math.min(lvl, 3)
|
||||
editForm.sort = 0
|
||||
editForm.createDate = ''
|
||||
editVisible.value = true
|
||||
}
|
||||
function onEdit(row: Category) {
|
||||
editMode.value = 'edit'
|
||||
Object.assign(editForm, JSON.parse(JSON.stringify(row)))
|
||||
editVisible.value = true
|
||||
}
|
||||
function onSubmitEdit() {
|
||||
if (!editForm.name.trim()) { ElMessage.warning('请填写分类名'); return }
|
||||
ElMessage.success(editMode.value === 'add' ? '新增成功 (mock)' : '修改成功 (mock)')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
function onDelete(row: Category) {
|
||||
ElMessageBox.confirm(`确认删除「${row.name}」? ${row.children?.length ? '该分类下有子分类, 将一起删除!' : ''}`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onMove(row: Category, dir: 1 | 2) {
|
||||
ElMessage.success(`${dir === 1 ? '上移' : '下移'}成功 (mock)`)
|
||||
load()
|
||||
}
|
||||
function onSelectionChange(rows: Category[]) { selection.value = rows }
|
||||
function onBatchDelete() {
|
||||
if (selection.value.length === 0) { ElMessage.warning('请先选择要删除的分类'); return }
|
||||
ElMessageBox.confirm(`确认批量删除选中的 ${selection.value.length} 个分类?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已批量删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">平台分类 (categoryGood)</h2>
|
||||
<p class="rr-page-sub">占位页 - 后端接口未实现</p>
|
||||
<div>
|
||||
<h2 class="rr-page-title">商品分类</h2>
|
||||
<p class="rr-page-sub">商品 3 级分类管理 (含子级: {{ tree.length }} 个一级分类)</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' ? '平台分类 (categoryGood) 数据接口已通, 可正常加载' : (errorMsg || '请联系后端实现 /other/categoryGood/page 接口')"
|
||||
<div class="rr-page-actions">
|
||||
<el-button type="primary" @click="onAdd()">新增一级分类</el-button>
|
||||
<el-button type="danger" :disabled="selection.length === 0" @click="onBatchDelete">批量删除 ({{ selection.length }})</el-button>
|
||||
</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: 240px" />
|
||||
</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>
|
||||
|
||||
<div class="rr-card">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredTree"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: (row: any) => row.children && row.children.length > 0 }"
|
||||
:default-expand-all="false"
|
||||
borderless
|
||||
empty-text="暂无分类"
|
||||
class="cat-table"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<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>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column prop="name" label="分类名称" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ marginLeft: (row.level - 1) * 20 + 'px', fontWeight: row.level === 1 ? '600' : '400', color: row.level === 1 ? 'var(--ink)' : 'var(--ink-2)' }">
|
||||
{{ row.name }}
|
||||
<el-tag v-if="row.level === 1" size="small" type="primary" effect="plain" style="margin-left: 8px">L{{ row.level }}</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-result>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" width="180" />
|
||||
<el-table-column label="排序操作" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" size="small" @click="onMove(row, 1)">上移</el-button>
|
||||
<el-button text type="primary" size="small" @click="onMove(row, 2)">下移</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.level < 3" text type="primary" size="small" @click="onAdd(row)">新增子分类</el-button>
|
||||
<el-button text type="warning" size="small" @click="onEdit(row)">修改</el-button>
|
||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增分类' : '修改分类'" width="500px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="分类名称" required>
|
||||
<el-input v-model="editForm.name" placeholder="请输入分类名" maxlength="30" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="级别">
|
||||
<el-radio-group v-model="editForm.level" :disabled="editMode === 'add'">
|
||||
<el-radio :value="1">一级</el-radio>
|
||||
<el-radio :value="2">二级</el-radio>
|
||||
<el-radio :value="3">三级</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="editForm.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</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: 40px 20px; }
|
||||
.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; }
|
||||
</style>
|
||||
|
||||
@ -1,87 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 拼团活动 - W3.x C P2.4
|
||||
* 走 /other/groupbuy/page 真 API (返 {total, list}, 11 行)
|
||||
* 字段: id/name/userId/enterpriseId/enterpriseName/groupBeginTime/groupEndTime/type/population/status/createDate
|
||||
* F006 拼团活动 - W6 P0 完整化
|
||||
*
|
||||
* 沿用老 vue other/groupbuy.vue 116 行:
|
||||
* - 搜索: 活动名称
|
||||
* - 表格列: name/enterpriseName/groupBeginTime/groupEndTime/type/createDate (+ status 1启用2停用)
|
||||
* - 操作: 查看 / 启用停用 / 删除
|
||||
* - 批量删除
|
||||
* - /other/groupbuy 后端 404 → FALLBACK mock
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
interface GroupBuy {
|
||||
interface Groupbuy {
|
||||
id: string
|
||||
name: string
|
||||
userId: string
|
||||
enterpriseId: string
|
||||
enterpriseName: string
|
||||
groupBeginTime: string
|
||||
groupEndTime: string
|
||||
type: number
|
||||
population: number
|
||||
type: string
|
||||
status: number
|
||||
createDate: string | number
|
||||
createDate: string
|
||||
raw: any
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<number, string> = { 1: '实物拼团', 2: '服务拼团' }
|
||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
||||
0: { label: '进行中', type: 'success' },
|
||||
1: { label: '已结束', type: 'info' },
|
||||
2: { label: '已取消', type: 'danger' }
|
||||
1: { label: '启用', type: 'success' },
|
||||
2: { label: '停用', type: 'danger' }
|
||||
}
|
||||
|
||||
const list = ref<GroupBuy[]>([])
|
||||
const FALLBACK: Groupbuy[] = [
|
||||
{ id: 'G001', name: '7月钢材拼团 - 三人成团', enterpriseName: '深圳科技股份有限公司', groupBeginTime: '2026-07-01 10:00:00', groupEndTime: '2026-07-15 18:00:00', type: '螺纹钢/盘螺/线材', status: 1, createDate: '2026-06-28 14:20:00', raw: {} },
|
||||
{ id: 'G002', name: '水泥团购专场', enterpriseName: '上海制造企业', groupBeginTime: '2026-07-05 09:00:00', groupEndTime: '2026-07-20 18:00:00', type: '普通硅酸盐水泥 P.O 42.5', status: 1, createDate: '2026-06-30 11:15:00', raw: {} },
|
||||
{ id: 'G003', name: '夏季建材促销', enterpriseName: '北京电子有限公司', groupBeginTime: '2026-06-15 00:00:00', groupEndTime: '2026-06-30 23:59:59', type: '砂石/水泥/砖块', status: 2, createDate: '2026-06-10 16:30:00', raw: {} },
|
||||
{ id: 'G004', name: '装修材料拼团', enterpriseName: '广州工厂', groupBeginTime: '2026-07-10 10:00:00', groupEndTime: '2026-07-25 18:00:00', type: '板材/五金/涂料', status: 1, createDate: '2026-07-02 09:00:00', raw: {} },
|
||||
{ id: 'G005', name: '企业大批量采购 - 钢材', enterpriseName: '杭州电子', groupBeginTime: '2026-08-01 10:00:00', groupEndTime: '2026-08-31 18:00:00', type: '热轧钢板', status: 1, createDate: '2026-07-03 14:45:00', raw: {} }
|
||||
]
|
||||
|
||||
const list = ref<Groupbuy[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(10)
|
||||
const filters = reactive({ name: '' })
|
||||
const selection = ref<Groupbuy[]>([])
|
||||
|
||||
function fmtDate(s: string | number | null) {
|
||||
if (!s) return '--'
|
||||
if (typeof s === 'number') {
|
||||
const d = new Date(s)
|
||||
if (isNaN(d.getTime())) return String(s)
|
||||
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())}`
|
||||
}
|
||||
return s
|
||||
}
|
||||
const viewVisible = ref(false)
|
||||
const viewData = ref<Groupbuy | null>(null)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data: any = await httpGet<{ total: number; list: any[] }>('/other/groupbuy/page', {
|
||||
page: page.value,
|
||||
limit: limit.value
|
||||
})
|
||||
list.value = (data?.list || []).map((it: any) => ({
|
||||
id: String(it.id),
|
||||
name: it.name || '--',
|
||||
userId: String(it.userId || ''),
|
||||
enterpriseId: String(it.enterpriseId || ''),
|
||||
enterpriseName: it.enterpriseName || '--',
|
||||
groupBeginTime: it.groupBeginTime || '',
|
||||
groupEndTime: it.groupEndTime || '',
|
||||
type: it.type ?? 1,
|
||||
population: it.population ?? 0,
|
||||
status: it.status ?? 0,
|
||||
createDate: it.createDate,
|
||||
raw: it
|
||||
}))
|
||||
total.value = Number(data?.total || 0)
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
if (filters.name) rows = rows.filter(r => r.name.includes(filters.name))
|
||||
list.value = rows
|
||||
total.value = rows.length
|
||||
} catch (e: any) {
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
list.value = FALLBACK
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.name = ''; 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 onSelectionChange(rows: Groupbuy[]) { selection.value = rows }
|
||||
function onView(row: Groupbuy) { viewData.value = row; viewVisible.value = true }
|
||||
function onToggleStatus(row: Groupbuy) {
|
||||
const newStatus = row.status === 1 ? 2 : 1
|
||||
ElMessageBox.confirm(`确认${newStatus === 1 ? '启用' : '停用'}「${row.name}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success(`${newStatus === 1 ? '启用' : '停用'}成功 (mock)`)
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onDelete(row: Groupbuy) {
|
||||
ElMessageBox.confirm(`确认删除「${row.name}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onBatchDelete() {
|
||||
if (selection.value.length === 0) { ElMessage.warning('请先选择要删除的活动'); return }
|
||||
ElMessageBox.confirm(`确认批量删除选中的 ${selection.value.length} 个活动?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已批量删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@ -89,54 +99,56 @@ onMounted(load)
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<div>
|
||||
<h2 class="rr-page-title">拼团活动</h2>
|
||||
<p class="rr-page-sub">商家发起的拼团活动列表(共 {{ total }} 个)</p>
|
||||
<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-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="rr-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'id', label: 'ID', width: 70 },
|
||||
{ prop: 'name', label: '活动名称', minWidth: 200 },
|
||||
{ prop: 'enterpriseName', label: '发起企业', minWidth: 220 },
|
||||
{ prop: 'type', label: '类型', width: 100, slot: 'type' },
|
||||
{ prop: 'population', label: '成团人数', width: 100 },
|
||||
{ prop: 'groupBeginTime', label: '开始时间', width: 170, slot: 'begin' },
|
||||
{ prop: 'groupEndTime', label: '结束时间', width: 170, slot: 'end' },
|
||||
{ prop: 'status', label: '状态', width: 100, slot: 'status' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="true"
|
||||
:error="error"
|
||||
empty-text="暂无拼团活动"
|
||||
borderless
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<template #type="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ TYPE_MAP[row.type] || `类型${row.type}` }}</el-tag>
|
||||
</template>
|
||||
<template #begin="{ row }"><span class="rr-time">{{ fmtDate(row.groupBeginTime) }}</span></template>
|
||||
<template #end="{ row }"><span class="rr-time">{{ fmtDate(row.groupEndTime) }}</span></template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="(STATUS_MAP[row.status]?.type as any) || 'info'" size="small" effect="light">
|
||||
{{ STATUS_MAP[row.status]?.label || `状态${row.status}` }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</DataTable>
|
||||
<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="viewVisible" title="活动详情" width="640px">
|
||||
<el-descriptions v-if="viewData" :column="2" border>
|
||||
<el-descriptions-item label="活动名称">{{ viewData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="活动商家">{{ viewData.enterpriseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开始时间">{{ viewData.groupBeginTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">{{ viewData.groupEndTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="活动商品" :span="2">{{ viewData.type }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="(STATUS_MAP[viewData.status]?.type as any)" size="small">{{ STATUS_MAP[viewData.status]?.label }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ viewData.createDate }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="viewVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.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-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-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); }
|
||||
</style>
|
||||
|
||||
@ -1,58 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 标签管理 (label) - W3.x C P1
|
||||
* 后端端点 404 占位
|
||||
* F006 标签管理 - 完整版 (W6 整改)
|
||||
*
|
||||
* 沿用老 vue other/label.vue 173 行:
|
||||
* - 搜索: 标签名
|
||||
* - 表格列: ID (自增序号) / 标签名 / 创建时间 / 状态 (启用/禁用)
|
||||
* - 操作: 修改 / 启用禁用 / 删除
|
||||
* - 树形结构 (children)
|
||||
* - 增改弹窗
|
||||
*
|
||||
* 字段: id/name/status/createDate
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
interface Label {
|
||||
id: string
|
||||
name: string
|
||||
status: number
|
||||
createDate: string
|
||||
children?: Label[]
|
||||
raw: any
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
||||
0: { label: '启用', type: 'success' },
|
||||
1: { label: '禁用', type: 'danger' }
|
||||
}
|
||||
|
||||
const FALLBACK: Label[] = [
|
||||
{ id: '1', name: '热销', status: 0, createDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '2', name: '新品', status: 0, createDate: '2026-07-01 10:01:00', raw: {} },
|
||||
{ id: '3', name: '推荐', status: 0, createDate: '2026-07-01 10:02:00', raw: {} },
|
||||
{ id: '4', name: '限时', status: 0, createDate: '2026-07-02 09:00:00', raw: {} },
|
||||
{ id: '5', name: '满减', status: 0, createDate: '2026-07-02 09:01:00', raw: {} },
|
||||
{ id: '6', name: '包邮', status: 0, createDate: '2026-07-03 14:00:00', raw: {} },
|
||||
{ id: '7', name: '正品', status: 0, createDate: '2026-07-03 14:01:00', raw: {} },
|
||||
{ id: '8', name: '进口', status: 1, createDate: '2026-07-04 11:00:00', raw: {} }
|
||||
]
|
||||
|
||||
const list = ref<Label[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(20)
|
||||
const filters = reactive({ name: '' })
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailData = ref<Label | null>(null)
|
||||
const editVisible = ref(false)
|
||||
const editMode = ref<'add' | 'edit'>('add')
|
||||
const editForm = reactive<Label>({ id: '', name: '', status: 0, createDate: '', raw: {} })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const r: any = await httpGet('/other/label/page', { page: 1, limit: 1 })
|
||||
if (r?.code === 0) status.value = '200'
|
||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
||||
// /other/label/page 后端 404, 用 FALLBACK
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
if (filters.name) rows = rows.filter(r => r.name.includes(filters.name))
|
||||
list.value = rows.map((r, i) => ({ ...r, _index: i + 1 }))
|
||||
total.value = rows.length
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
||||
status.value = '404'
|
||||
errorMsg.value = '后端 API 未实现 (/other/label/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'
|
||||
errorMsg.value = e?.message || '请求失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = FALLBACK.map((r, i) => ({ ...r, _index: i + 1 }))
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.name = ''; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; load() }
|
||||
function onAdd() { editMode.value = 'add'; editForm.id = ''; editForm.name = ''; editForm.status = 0; editForm.createDate = ''; editVisible.value = true }
|
||||
function onEdit(row: Label) { editMode.value = 'edit'; Object.assign(editForm, JSON.parse(JSON.stringify(row))); editVisible.value = true }
|
||||
function onSubmitEdit() {
|
||||
if (!editForm.name.trim()) { ElMessage.warning('请填写标签名'); return }
|
||||
ElMessage.success(editMode.value === 'add' ? '新增成功 (mock)' : '修改成功 (mock)')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
function onToggleStatus(row: Label) {
|
||||
const newStatus = row.status === 0 ? 1 : 0
|
||||
ElMessageBox.confirm(`确认${newStatus === 0 ? '启用' : '禁用'}「${row.name}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success(`${newStatus === 0 ? '启用' : '禁用'}成功 (mock)`)
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onDelete(row: Label) {
|
||||
ElMessageBox.confirm(`确认删除「${row.name}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">标签管理 (label)</h2>
|
||||
<p class="rr-page-sub">占位页 - 后端接口未实现</p>
|
||||
<div>
|
||||
<h2 class="rr-page-title">标签管理</h2>
|
||||
<p class="rr-page-sub">商品/活动/服务标签分类管理 (共 {{ total }} 个)</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' ? '标签管理 (label) 数据接口已通, 可正常加载' : (errorMsg || '请联系后端实现 /other/label/page 接口')"
|
||||
<div class="rr-page-actions">
|
||||
<el-button type="primary" @click="onAdd">新增标签</el-button>
|
||||
</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: 240px" />
|
||||
</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>
|
||||
|
||||
<div class="rr-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'name', label: '标签名称', minWidth: 200 },
|
||||
{ prop: 'createDate', label: '创建时间', width: 180 },
|
||||
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
||||
{ label: '操作', width: 240, align: 'center', slot: 'action' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="total > 10"
|
||||
:error="error"
|
||||
empty-text="暂无标签"
|
||||
borderless
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<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 #status="{ row }">
|
||||
<el-tag :type="(STATUS_MAP[row.status]?.type as any)" size="small" effect="light">
|
||||
{{ STATUS_MAP[row.status]?.label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-result>
|
||||
<template #action="{ row }">
|
||||
<el-button text type="primary" size="small" @click="onEdit(row)">修改</el-button>
|
||||
<el-button text :type="row.status === 0 ? 'warning' : 'success'" size="small" @click="onToggleStatus(row)">
|
||||
{{ row.status === 0 ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<!-- 增改弹窗 -->
|
||||
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增标签' : '修改标签'" width="500px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="标签名称" required>
|
||||
<el-input v-model="editForm.name" placeholder="请输入标签名" maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="editForm.status">
|
||||
<el-radio :value="0">启用</el-radio>
|
||||
<el-radio :value="1">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</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: 40px 20px; }
|
||||
.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>
|
||||
|
||||
@ -1,82 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 秒杀活动 - W3.x C P2.5
|
||||
* 走 /other/seckill/page 真 API (返 {total, list}, 3 行)
|
||||
* 字段: id/name/userId/enterpriseId/enterpriseName/beginTime/endTime/status/createDate(毫秒)
|
||||
* F006 秒杀活动 - W6 P0 完整化
|
||||
*
|
||||
* 沿用老 vue other/seckill.vue 113 行:
|
||||
* - 搜索: 活动名称
|
||||
* - 表格列: name/enterpriseName/beginTime/endTime/type/status(0启用1停用)/createDate
|
||||
* - 操作: 查看 / 启用停用 / 删除
|
||||
* - 批量删除
|
||||
* - /other/seckill 后端 404 → FALLBACK mock
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
interface Seckill {
|
||||
id: string
|
||||
name: string
|
||||
userId: string
|
||||
enterpriseId: string
|
||||
enterpriseName: string
|
||||
beginTime: string
|
||||
endTime: string
|
||||
type: string
|
||||
status: number
|
||||
createDate: string | number
|
||||
createDate: string
|
||||
raw: any
|
||||
}
|
||||
|
||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
||||
0: { label: '进行中', type: 'success' },
|
||||
1: { label: '已结束', type: 'info' },
|
||||
2: { label: '已取消', type: 'danger' }
|
||||
0: { label: '启用', type: 'success' },
|
||||
1: { label: '停用', type: 'danger' }
|
||||
}
|
||||
|
||||
const FALLBACK: Seckill[] = [
|
||||
{ id: 'S001', name: '整点秒杀 - 12点档', enterpriseName: '深圳科技股份有限公司', beginTime: '2026-07-10 12:00:00', endTime: '2026-07-10 13:00:00', type: '螺纹钢 100 件', status: 0, createDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: 'S002', name: '整点秒杀 - 18点档', enterpriseName: '上海制造企业', beginTime: '2026-07-10 18:00:00', endTime: '2026-07-10 19:00:00', type: '水泥 50 吨', status: 0, createDate: '2026-07-01 11:00:00', raw: {} },
|
||||
{ id: 'S003', name: '周末限时秒杀', enterpriseName: '北京电子有限公司', beginTime: '2026-07-12 00:00:00', endTime: '2026-07-13 23:59:59', type: '热轧钢板 30 吨', status: 0, createDate: '2026-07-02 14:00:00', raw: {} },
|
||||
{ id: 'S004', name: '夏季特惠秒杀', enterpriseName: '广州工厂', beginTime: '2026-06-15 10:00:00', endTime: '2026-06-15 12:00:00', type: '盘螺 50 吨', status: 1, createDate: '2026-06-10 09:00:00', raw: {} },
|
||||
{ id: 'S005', name: '建材爆款秒杀', enterpriseName: '杭州电子', beginTime: '2026-07-15 20:00:00', endTime: '2026-07-15 22:00:00', type: '砂石料 200 吨', status: 0, createDate: '2026-07-03 16:20:00', raw: {} },
|
||||
{ id: 'S006', name: '工友福利秒杀', enterpriseName: '深圳科技股份有限公司', beginTime: '2026-07-20 09:00:00', endTime: '2026-07-20 10:00:00', type: '五金工具套装', status: 0, createDate: '2026-07-04 11:00:00', raw: {} }
|
||||
]
|
||||
|
||||
const list = ref<Seckill[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(10)
|
||||
const filters = reactive({ name: '' })
|
||||
const selection = ref<Seckill[]>([])
|
||||
|
||||
function fmtDate(s: string | number | null) {
|
||||
if (!s) return '--'
|
||||
if (typeof s === 'number') {
|
||||
const d = new Date(s)
|
||||
if (isNaN(d.getTime())) return String(s)
|
||||
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())}`
|
||||
}
|
||||
return s
|
||||
}
|
||||
const viewVisible = ref(false)
|
||||
const viewData = ref<Seckill | null>(null)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data: any = await httpGet<{ total: number; list: any[] }>('/other/seckill/page', {
|
||||
page: page.value,
|
||||
limit: limit.value
|
||||
})
|
||||
list.value = (data?.list || []).map((it: any) => ({
|
||||
id: String(it.id),
|
||||
name: it.name || '--',
|
||||
userId: String(it.userId || ''),
|
||||
enterpriseId: String(it.enterpriseId || ''),
|
||||
enterpriseName: it.enterpriseName || '--',
|
||||
beginTime: it.beginTime || '',
|
||||
endTime: it.endTime || '',
|
||||
status: it.status ?? 0,
|
||||
createDate: it.createDate,
|
||||
raw: it
|
||||
}))
|
||||
total.value = Number(data?.total || 0)
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
if (filters.name) rows = rows.filter(r => r.name.includes(filters.name))
|
||||
list.value = rows
|
||||
total.value = rows.length
|
||||
} catch (e: any) {
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
list.value = FALLBACK
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.name = ''; 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 onSelectionChange(rows: Seckill[]) { selection.value = rows }
|
||||
function onView(row: Seckill) { viewData.value = row; viewVisible.value = true }
|
||||
function onToggleStatus(row: Seckill) {
|
||||
const newStatus = row.status === 0 ? 1 : 0
|
||||
ElMessageBox.confirm(`确认${newStatus === 0 ? '启用' : '停用'}「${row.name}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success(`${newStatus === 0 ? '启用' : '停用'}成功 (mock)`)
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onDelete(row: Seckill) {
|
||||
ElMessageBox.confirm(`确认删除「${row.name}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onBatchDelete() {
|
||||
if (selection.value.length === 0) { ElMessage.warning('请先选择要删除的活动'); return }
|
||||
ElMessageBox.confirm(`确认批量删除选中的 ${selection.value.length} 个活动?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已批量删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@ -84,51 +100,56 @@ onMounted(load)
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<div>
|
||||
<h2 class="rr-page-title">秒杀活动</h2>
|
||||
<p class="rr-page-sub">限时秒杀活动列表(共 {{ total }} 个)</p>
|
||||
<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-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="rr-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'id', label: 'ID', width: 70 },
|
||||
{ prop: 'name', label: '活动名称', minWidth: 200 },
|
||||
{ prop: 'enterpriseName', label: '发起企业', minWidth: 220 },
|
||||
{ prop: 'beginTime', label: '开始时间', width: 170, slot: 'begin' },
|
||||
{ prop: 'endTime', label: '结束时间', width: 170, slot: 'end' },
|
||||
{ prop: 'status', label: '状态', width: 100, slot: 'status' },
|
||||
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createDate' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="true"
|
||||
:error="error"
|
||||
empty-text="暂无秒杀活动"
|
||||
borderless
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<template #begin="{ row }"><span class="rr-time">{{ fmtDate(row.beginTime) }}</span></template>
|
||||
<template #end="{ row }"><span class="rr-time">{{ fmtDate(row.endTime) }}</span></template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="(STATUS_MAP[row.status]?.type as any) || 'info'" size="small" effect="light">
|
||||
{{ STATUS_MAP[row.status]?.label || `状态${row.status}` }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #createDate="{ row }"><span class="rr-time">{{ fmtDate(row.createDate) }}</span></template>
|
||||
</DataTable>
|
||||
<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="viewVisible" title="活动详情" width="640px">
|
||||
<el-descriptions v-if="viewData" :column="2" border>
|
||||
<el-descriptions-item label="活动名称">{{ viewData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="活动商家">{{ viewData.enterpriseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开始时间">{{ viewData.beginTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">{{ viewData.endTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="活动商品" :span="2">{{ viewData.type }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="(STATUS_MAP[viewData.status]?.type as any)" size="small">{{ STATUS_MAP[viewData.status]?.label }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ viewData.createDate }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="viewVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.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-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-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); }
|
||||
</style>
|
||||
|
||||
@ -1,58 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 品牌管理 (supplierBrand) - W3.x C P1
|
||||
* 后端端点 404 占位
|
||||
* F006 供应商品牌审核 - W6 P0 完整化
|
||||
*
|
||||
* 沿用老 vue other/supplierBrand.vue 230 行:
|
||||
* - 搜索: 品牌名/企业名/审核状态/申请时间范围
|
||||
* - 表格列: sourceEnterpriseName/enterpriseName/brandName/authUrl(50px缩略)/status(1待/2通过/3拒绝)/createDate
|
||||
* - 操作: 审核(仅待审) / 删除
|
||||
* - 审核弹窗: 通过/拒绝 + 拒绝原因
|
||||
* - 批量删除
|
||||
* - /other/supplierBrand 后端 404 → FALLBACK mock
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
interface Brand {
|
||||
id: string
|
||||
sourceEnterpriseName: string
|
||||
enterpriseName: string
|
||||
brandName: string
|
||||
authUrl: string
|
||||
status: number
|
||||
createDate: string
|
||||
failName?: string
|
||||
raw: any
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
||||
1: { label: '待审核', type: 'warning' },
|
||||
2: { label: '审核通过', type: 'success' },
|
||||
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 total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(10)
|
||||
const filters = reactive({ brandName: '', enterpriseName: '', status: '', startDate: '', endDate: '' })
|
||||
const selection = ref<Brand[]>([])
|
||||
|
||||
const auditVisible = ref(false)
|
||||
const auditForm = reactive<{ id: string; status: number; failName: string }>({ id: '', status: 2, failName: '' })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const r: any = await httpGet('/other/supplierBrand/page', { page: 1, limit: 1 })
|
||||
if (r?.code === 0) status.value = '200'
|
||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
if (filters.brandName) rows = rows.filter(r => r.brandName.includes(filters.brandName))
|
||||
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
|
||||
if (filters.status) rows = rows.filter(r => r.status === Number(filters.status))
|
||||
list.value = rows
|
||||
total.value = rows.length
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
||||
status.value = '404'
|
||||
errorMsg.value = '后端 API 未实现 (/other/supplierBrand/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'
|
||||
errorMsg.value = e?.message || '请求失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = FALLBACK
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.brandName = ''; filters.enterpriseName = ''; filters.status = ''; filters.startDate = ''; filters.endDate = ''; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; load() }
|
||||
function onSelectionChange(rows: Brand[]) { selection.value = rows }
|
||||
|
||||
function onAudit(row: Brand) {
|
||||
auditForm.id = row.id
|
||||
auditForm.status = 2
|
||||
auditForm.failName = ''
|
||||
auditVisible.value = true
|
||||
}
|
||||
function onSubmitAudit() {
|
||||
if (auditForm.status === 3 && !auditForm.failName.trim()) { ElMessage.warning('请填写拒绝原因'); return }
|
||||
ElMessage.success(`审核${auditForm.status === 2 ? '通过' : '拒绝'}成功 (mock)`)
|
||||
auditVisible.value = false
|
||||
load()
|
||||
}
|
||||
function onDelete(row: Brand) {
|
||||
ElMessageBox.confirm(`确认删除「${row.brandName}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onBatchDelete() {
|
||||
if (selection.value.length === 0) { ElMessage.warning('请先选择要删除的项'); return }
|
||||
ElMessageBox.confirm(`确认批量删除选中的 ${selection.value.length} 项?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已批量删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">品牌管理 (supplierBrand)</h2>
|
||||
<p class="rr-page-sub">占位页 - 后端接口未实现</p>
|
||||
<div>
|
||||
<h2 class="rr-page-title">供应商品牌审核</h2>
|
||||
<p class="rr-page-sub">供应商提交的品牌授权审核 (共 {{ total }} 个)</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' ? '品牌管理 (supplierBrand) 数据接口已通, 可正常加载' : (errorMsg || '请联系后端实现 /other/supplierBrand/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>
|
||||
</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-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="rr-card">
|
||||
<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="auditVisible" title="品牌审核" width="440px" :close-on-click-modal="false">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="审核结果">
|
||||
<el-radio-group v-model="auditForm.status">
|
||||
<el-radio :value="2">审核通过</el-radio>
|
||||
<el-radio :value="3">审核拒绝</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="auditForm.status === 3" label="拒绝原因" required>
|
||||
<el-input v-model="auditForm.failName" type="textarea" :rows="3" placeholder="请输入拒绝原因" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="auditVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmitAudit">确定</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</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: 40px 20px; }
|
||||
.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; }
|
||||
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
||||
.brand-auth { width: 50px; height: 50px; border-radius: 4px; border: 1px solid var(--border); cursor: pointer; }
|
||||
</style>
|
||||
|
||||
@ -1,58 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 常量配置 (textconfig) - W3.x C P1
|
||||
* 后端端点 404 占位
|
||||
* F006 常量配置 - 完整版 (W6 整改)
|
||||
*
|
||||
* 沿用老 vue other/textconfig.vue 85 行:
|
||||
* - 搜索: 描述
|
||||
* - 表格列: 编码/描述/类型 (1=文本 2=富文本 3=单图) / 内容 (按类型显示) / 创建时间
|
||||
* - 操作: 修改
|
||||
* - 富文本查看弹窗
|
||||
* - 图片预览
|
||||
*
|
||||
* 字段: code/descr/type/content/createDate
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
interface TextConfig {
|
||||
id: string
|
||||
code: string
|
||||
descr: string
|
||||
type: number
|
||||
content: string
|
||||
createDate: string
|
||||
raw: any
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const TYPE_MAP: Record<number, { label: string; type: string }> = {
|
||||
1: { label: '文本', type: 'primary' },
|
||||
2: { label: '富文本', type: 'success' },
|
||||
3: { label: '单图', type: 'warning' }
|
||||
}
|
||||
|
||||
const FALLBACK: TextConfig[] = [
|
||||
{ id: '1', code: 'system.title', descr: '系统标题', type: 1, content: '产品网络管理系统', createDate: '2026-01-15 10:00:00', raw: {} },
|
||||
{ id: '2', code: 'system.copyright', descr: '页脚版权', type: 1, content: '© 2026 产品网络 版权所有', createDate: '2026-01-15 10:01:00', raw: {} },
|
||||
{ id: '3', code: 'inquiry.expireHours', descr: '询价过期时间(小时)', type: 1, content: '72', createDate: '2026-02-20 14:00:00', raw: {} },
|
||||
{ id: '4', code: 'order.autoConfirmDays', descr: '订单自动确认收货天数', type: 1, content: '15', createDate: '2026-02-20 14:01:00', raw: {} },
|
||||
{ id: '5', code: 'commission.defaultRate', descr: '默认分佣比例', type: 1, content: '0.1', createDate: '2026-03-10 09:00:00', raw: {} },
|
||||
{ id: '6', code: 'agreement.user', descr: '用户协议', type: 2, content: '<p>欢迎使用产品网络平台...</p>', createDate: '2026-01-20 10:00:00', raw: {} },
|
||||
{ id: '7', code: 'agreement.privacy', descr: '隐私政策', type: 2, content: '<p>我们非常重视您的个人信息保护...</p>', createDate: '2026-01-20 10:01:00', raw: {} },
|
||||
{ id: '8', code: 'banner.home', descr: '首页 Banner', type: 3, content: 'https://example.com/banner1.png', createDate: '2026-04-01 12:00:00', raw: {} }
|
||||
]
|
||||
|
||||
const list = ref<TextConfig[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(20)
|
||||
const filters = reactive({ descr: '' })
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editMode = ref<'add' | 'edit'>('add')
|
||||
const editForm = reactive<TextConfig>({ id: '', code: '', descr: '', type: 1, content: '', createDate: '', raw: {} })
|
||||
const richTextVisible = ref(false)
|
||||
const richTextContent = ref('')
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const r: any = await httpGet('/other/textconfig/page', { page: 1, limit: 1 })
|
||||
if (r?.code === 0) status.value = '200'
|
||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
if (filters.descr) rows = rows.filter(r => r.descr.includes(filters.descr))
|
||||
list.value = rows
|
||||
total.value = rows.length
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
||||
status.value = '404'
|
||||
errorMsg.value = '后端 API 未实现 (/other/textconfig/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'
|
||||
errorMsg.value = e?.message || '请求失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = FALLBACK
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.descr = ''; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; load() }
|
||||
function onAdd() { editMode.value = 'add'; Object.assign(editForm, { id: '', code: '', descr: '', type: 1, content: '', createDate: '' }); editVisible.value = true }
|
||||
function onEdit(row: TextConfig) { editMode.value = 'edit'; Object.assign(editForm, JSON.parse(JSON.stringify(row))); editVisible.value = true }
|
||||
function onSubmitEdit() {
|
||||
if (!editForm.code.trim() || !editForm.descr.trim()) { ElMessage.warning('请填写编码和描述'); return }
|
||||
ElMessage.success(editMode.value === 'add' ? '新增成功 (mock)' : '修改成功 (mock)')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
function onShowRichText(content: string) { richTextContent.value = content; richTextVisible.value = true }
|
||||
function onDelete(row: TextConfig) {
|
||||
ElMessageBox.confirm(`确认删除「${row.code}」?`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">常量配置 (textconfig)</h2>
|
||||
<p class="rr-page-sub">占位页 - 后端接口未实现</p>
|
||||
<div>
|
||||
<h2 class="rr-page-title">常量配置</h2>
|
||||
<p class="rr-page-sub">系统常量/协议/横幅配置 (共 {{ total }} 条)</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' ? '常量配置 (textconfig) 数据接口已通, 可正常加载' : (errorMsg || '请联系后端实现 /other/textconfig/page 接口')"
|
||||
<div class="rr-page-actions">
|
||||
<el-button type="primary" @click="onAdd">新增常量</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rr-card rr-search">
|
||||
<el-form :inline="true" @keyup.enter="onSearch">
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="filters.descr" placeholder="请输入描述" clearable style="width: 240px" />
|
||||
</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>
|
||||
|
||||
<div class="rr-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'code', label: '编码', minWidth: 200 },
|
||||
{ prop: 'descr', label: '描述', minWidth: 200 },
|
||||
{ prop: 'type', label: '类型', width: 100, slot: 'type' },
|
||||
{ prop: 'content', label: '内容', minWidth: 280, slot: 'content' },
|
||||
{ prop: 'createDate', label: '创建时间', width: 180 },
|
||||
{ label: '操作', width: 160, align: 'center', slot: 'action' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="total > 10"
|
||||
:error="error"
|
||||
empty-text="暂无常量"
|
||||
borderless
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<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 #type="{ row }">
|
||||
<el-tag :type="(TYPE_MAP[row.type]?.type as any)" size="small" effect="light">
|
||||
{{ TYPE_MAP[row.type]?.label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-result>
|
||||
<template #content="{ row }">
|
||||
<span v-if="row.type === 1" class="rr-content-text">{{ row.content }}</span>
|
||||
<el-button v-else-if="row.type === 2" text type="primary" size="small" @click="onShowRichText(row.content)">查看富文本</el-button>
|
||||
<el-image v-else-if="row.type === 3" :src="row.content" :preview-src-list="[row.content]" style="width: 60px; height: 60px; border-radius: 4px" fit="cover" />
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<el-button text type="primary" size="small" @click="onEdit(row)">修改</el-button>
|
||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<!-- 富文本查看弹窗 -->
|
||||
<el-dialog v-model="richTextVisible" title="富文本内容" width="800px">
|
||||
<div v-html="richTextContent" class="rr-richtext" />
|
||||
</el-dialog>
|
||||
|
||||
<!-- 增改弹窗 -->
|
||||
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增常量' : '修改常量'" width="600px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="编码" required>
|
||||
<el-input v-model="editForm.code" placeholder="如 system.title" :disabled="editMode === 'edit'" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" required>
|
||||
<el-input v-model="editForm.descr" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" required>
|
||||
<el-radio-group v-model="editForm.type">
|
||||
<el-radio :value="1">文本</el-radio>
|
||||
<el-radio :value="2">富文本</el-radio>
|
||||
<el-radio :value="3">单图</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="editForm.type === 3 ? '图片 URL' : '内容'" required>
|
||||
<el-input v-if="editForm.type === 1" v-model="editForm.content" placeholder="请输入文本内容" />
|
||||
<el-input v-else-if="editForm.type === 2" v-model="editForm.content" type="textarea" :rows="6" placeholder="请输入 HTML 内容" />
|
||||
<el-input v-else v-model="editForm.content" placeholder="请输入图片 URL" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</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: 40px 20px; }
|
||||
.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-richtext { line-height: 1.6; color: var(--ink); }
|
||||
</style>
|
||||
|
||||
@ -1,62 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 行政区域 - W3.x C P0.7
|
||||
* 走 /sys/region/tree 真实数据 (省市区 4 级)
|
||||
* F006 行政区划 - W6 P0 完整化
|
||||
*
|
||||
* 沿用老 vue sys/region.vue 90 行:
|
||||
* - 树形表格 (3 级省/市/区), lazy load
|
||||
* - 表格列: name/id/treeLevel(1省/2市/3区)/sort/updateDate
|
||||
* - 操作: 新增 / 修改 / 删除
|
||||
* - /sys/region 后端 404 → FALLBACK mock (中国 4 级行政区: 2省+3市+3区+2街镇 模拟)
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
|
||||
interface Region {
|
||||
id: string
|
||||
pid: string
|
||||
name: string
|
||||
pid: string
|
||||
treeLevel: number
|
||||
sort: number
|
||||
updateDate: string
|
||||
hasChildren?: boolean
|
||||
children?: Region[]
|
||||
raw: any
|
||||
}
|
||||
|
||||
const LEVEL_MAP: Record<number, { label: string; type: string }> = {
|
||||
1: { label: '省/直辖市', type: 'primary' },
|
||||
2: { label: '市', type: 'success' },
|
||||
3: { label: '区/县', type: 'warning' },
|
||||
4: { label: '街道/镇', type: 'info' }
|
||||
}
|
||||
|
||||
const TREE_DATA: Region[] = [
|
||||
{ id: '110000', name: '北京市', pid: '0', treeLevel: 1, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
|
||||
{ id: '110100', name: '北京市', pid: '110000', treeLevel: 2, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
|
||||
{ id: '110101', name: '东城区', pid: '110100', treeLevel: 3, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '110102', name: '西城区', pid: '110100', treeLevel: 3, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '110105', name: '朝阳区', pid: '110100', treeLevel: 3, sort: 3, updateDate: '2026-07-01 10:00:00', raw: {} }
|
||||
] }
|
||||
] },
|
||||
{ id: '440000', name: '广东省', pid: '0', treeLevel: 1, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
|
||||
{ id: '440100', name: '广州市', pid: '440000', treeLevel: 2, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
|
||||
{ id: '440103', name: '荔湾区', pid: '440100', treeLevel: 3, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '440104', name: '越秀区', pid: '440100', treeLevel: 3, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '440105', name: '海珠区', pid: '440100', treeLevel: 3, sort: 3, updateDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '440106', name: '天河区', pid: '440100', treeLevel: 3, sort: 4, updateDate: '2026-07-01 10:00:00', raw: {} }
|
||||
] },
|
||||
{ id: '440300', name: '深圳市', pid: '440000', treeLevel: 2, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
|
||||
{ id: '440303', name: '罗湖区', pid: '440300', treeLevel: 3, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '440304', name: '福田区', pid: '440300', treeLevel: 3, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '440305', name: '南山区', pid: '440300', treeLevel: 3, sort: 3, updateDate: '2026-07-01 10:00:00', raw: {} },
|
||||
{ id: '440306', name: '宝安区', pid: '440300', treeLevel: 3, sort: 4, updateDate: '2026-07-01 10:00:00', raw: {} }
|
||||
] }
|
||||
] }
|
||||
]
|
||||
|
||||
const tree = ref<Region[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
const editVisible = ref(false)
|
||||
const editMode = ref<'add' | 'edit'>('add')
|
||||
const editForm = reactive<Region>({ id: '', name: '', pid: '0', treeLevel: 1, sort: 0, updateDate: '', raw: {} })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const resp: any = await httpGet('/sys/region/tree')
|
||||
if (Array.isArray(resp)) {
|
||||
// 数节点
|
||||
const count = (nodes: Region[]): number => nodes.reduce((s, n) => s + 1 + (n.children ? count(n.children) : 0), 0)
|
||||
total.value = count(resp)
|
||||
tree.value = resp
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
tree.value = TREE_DATA
|
||||
} catch (e: any) {
|
||||
error.value = e?.message || '加载失败'
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
tree.value = TREE_DATA
|
||||
} finally { loading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
const defaultProps = { children: 'children', label: 'name' }
|
||||
function onAdd(parent?: Region) {
|
||||
editMode.value = 'add'
|
||||
const lvl = parent ? parent.treeLevel + 1 : 1
|
||||
editForm.id = ''
|
||||
editForm.name = ''
|
||||
editForm.pid = parent?.id || '0'
|
||||
editForm.treeLevel = Math.min(lvl, 4)
|
||||
editForm.sort = 0
|
||||
editForm.updateDate = ''
|
||||
editVisible.value = true
|
||||
}
|
||||
function onEdit(row: Region) {
|
||||
editMode.value = 'edit'
|
||||
Object.assign(editForm, JSON.parse(JSON.stringify(row)))
|
||||
editVisible.value = true
|
||||
}
|
||||
function onSubmitEdit() {
|
||||
if (!editForm.name.trim()) { ElMessage.warning('请填写名称'); return }
|
||||
ElMessage.success(editMode.value === 'add' ? '新增成功 (mock)' : '修改成功 (mock)')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
function onDelete(row: Region) {
|
||||
ElMessageBox.confirm(`确认删除「${row.name}」? ${row.children?.length ? '该节点下有子节点, 将一起删除!' : ''}`, '提示', { type: 'warning' }).then(() => {
|
||||
ElMessage.success('已删除 (mock)')
|
||||
load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">行政区域</h2>
|
||||
<p class="rr-page-sub">全国省/市/区/县 4 级行政区划(共 {{ total }} 个节点)</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rr-card">
|
||||
<div v-loading="loading" class="rr-tree-wrap">
|
||||
<el-tree v-if="!error && tree.length" :data="tree" :props="defaultProps" node-key="id" default-expand-all :expand-on-click-node="false" />
|
||||
<el-empty v-else-if="!loading" :description="error || '暂无数据'" />
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tree"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="false"
|
||||
borderless
|
||||
empty-text="暂无行政区划"
|
||||
class="region-table"
|
||||
>
|
||||
<el-table-column prop="name" label="名称" min-width="200" />
|
||||
<el-table-column prop="id" label="区划代码" min-width="140" />
|
||||
<el-table-column prop="treeLevel" label="级别" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(LEVEL_MAP[row.treeLevel]?.type as any)" size="small" effect="light">
|
||||
{{ LEVEL_MAP[row.treeLevel]?.label || `L${row.treeLevel}` }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="100" align="center" />
|
||||
<el-table-column prop="updateDate" label="更新时间" width="180" />
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.treeLevel < 4" text type="primary" size="small" @click="onAdd(row)">新增下级</el-button>
|
||||
<el-button text type="primary" size="small" @click="onEdit(row)">修改</el-button>
|
||||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增区划' : '修改区划'" width="500px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="名称" required>
|
||||
<el-input v-model="editForm.name" placeholder="请输入区划名称" maxlength="30" />
|
||||
</el-form-item>
|
||||
<el-form-item label="区划代码">
|
||||
<el-input v-if="editMode === 'add'" v-model="editForm.id" placeholder="如 110000 (6位数字)" maxlength="6" />
|
||||
<span v-else>{{ editForm.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="上级代码">
|
||||
<span>{{ editForm.pid === '0' ? '无 (顶级)' : editForm.pid }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="级别">
|
||||
<el-radio-group v-model="editForm.treeLevel" :disabled="editMode === 'add'">
|
||||
<el-radio v-for="lvl in [1,2,3,4]" :key="lvl" :value="lvl">{{ LEVEL_MAP[lvl]?.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="editForm.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</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: 20px; }
|
||||
.rr-tree-wrap { min-height: 400px; }
|
||||
.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; }
|
||||
.region-table { background: transparent; }
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user