feat: add admin web MVP

This commit is contained in:
王鹏
2026-07-07 16:18:23 +08:00
parent fa8f243486
commit bb325151d6
30 changed files with 4029 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { listGoodsOrders, listExpressOrders } from '../api/orderApi'
import { listGroupBuys } from '../api/groupBuyApi'
import { listSecondGoods } from '../api/secondGoodsApi'
import type { ExpressOrder, GoodsOrder, GroupBuy, SecondGoods } from '../types'
import { yuan } from '../utils/format'
const loading = ref(false)
const goodsOrders = ref<GoodsOrder[]>([])
const expressOrders = ref<ExpressOrder[]>([])
const groupBuys = ref<GroupBuy[]>([])
const pendingSecondGoods = ref<SecondGoods[]>([])
const todayOrderCount = computed(() => goodsOrders.value.length + expressOrders.value.length)
const receivableAmount = computed(() =>
goodsOrders.value.reduce((sum, item) => sum + item.payableAmountCent, 0)
+ expressOrders.value.reduce((sum, item) => sum + item.feeCent, 0)
)
const pendingConfirmCount = computed(() =>
goodsOrders.value.filter((item) => item.status === 'PENDING_CONFIRM').length
+ expressOrders.value.filter((item) => item.status === 'PENDING_CONFIRM').length
)
const pendingDeliveryCount = computed(() =>
goodsOrders.value.filter((item) => item.status === 'PENDING_DELIVERY').length
)
async function load() {
loading.value = true
try {
const [goods, express, groups, second] = await Promise.all([
listGoodsOrders(),
listExpressOrders(),
listGroupBuys(),
listSecondGoods('PENDING_REVIEW')
])
goodsOrders.value = goods
expressOrders.value = express
groupBuys.value = groups
pendingSecondGoods.value = second
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<template>
<h1 class="page-title">仪表盘</h1>
<div class="toolbar">
<el-button :icon="Refresh" :loading="loading" @click="load">刷新</el-button>
</div>
<div class="metric-grid">
<div class="metric">
<div class="metric-label">订单数</div>
<div class="metric-value">{{ todayOrderCount }}</div>
</div>
<div class="metric">
<div class="metric-label">应收金额</div>
<div class="metric-value">{{ yuan(receivableAmount) }}</div>
</div>
<div class="metric">
<div class="metric-label">待确认</div>
<div class="metric-value">{{ pendingConfirmCount }}</div>
</div>
<div class="metric">
<div class="metric-label">待配送</div>
<div class="metric-value">{{ pendingDeliveryCount }}</div>
</div>
<div class="metric">
<div class="metric-label">待审核闲置</div>
<div class="metric-value">{{ pendingSecondGoods.length }}</div>
</div>
</div>
<div class="panel" style="margin-top: 16px">
<div class="toolbar">
<strong>正在进行的团购</strong>
</div>
<el-table :data="groupBuys" border>
<el-table-column prop="title" label="标题" min-width="180" />
<el-table-column label="价格" width="120">
<template #default="{ row }">{{ yuan(row.priceCent) }}</template>
</el-table-column>
<el-table-column prop="soldCount" label="已售" width="90" />
<el-table-column prop="status" label="状态" width="120" />
</el-table>
</div>
</template>

View File

@@ -0,0 +1,134 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Plus, Refresh, Van } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { listGoodsOrders, listExpressOrders } from '../api/orderApi'
import {
assignExpressOrder,
assignGoodsOrder,
assignGroupBuyOrder,
createDeliveryUser,
listDeliveryOrders,
listDeliveryUsers
} from '../api/deliveryApi'
import type { DeliveryOrder, DeliveryUser, ExpressOrder, GoodsOrder } from '../types'
import { statusText } from '../utils/format'
const loading = ref(false)
const riders = ref<DeliveryUser[]>([])
const tasks = ref<DeliveryOrder[]>([])
const goodsOrders = ref<GoodsOrder[]>([])
const expressOrders = ref<ExpressOrder[]>([])
const riderForm = reactive({ userId: 3, name: '', phone: '', enabled: true })
const dispatchForm = reactive({ goodsOrderId: undefined as number | undefined, expressOrderId: undefined as number | undefined, groupBuyOrderId: undefined as number | undefined, riderId: undefined as number | undefined })
async function load() {
loading.value = true
try {
const [riderRows, taskRows, goodsRows, expressRows] = await Promise.all([
listDeliveryUsers(),
listDeliveryOrders(),
listGoodsOrders({ status: 'PENDING_DELIVERY' }),
listExpressOrders('PENDING_PICKUP')
])
riders.value = riderRows
tasks.value = taskRows
goodsOrders.value = goodsRows
expressOrders.value = expressRows
if (!dispatchForm.riderId && riderRows[0]) dispatchForm.riderId = riderRows[0].id
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function saveRider() {
try {
await createDeliveryUser(riderForm)
riderForm.name = ''
riderForm.phone = ''
ElMessage.success('已创建配送员')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
}
}
async function dispatch(type: 'goods' | 'express' | 'group') {
if (!dispatchForm.riderId) {
ElMessage.warning('请选择配送员')
return
}
try {
if (type === 'goods' && dispatchForm.goodsOrderId) {
await assignGoodsOrder(dispatchForm.goodsOrderId, dispatchForm.riderId)
}
if (type === 'express' && dispatchForm.expressOrderId) {
await assignExpressOrder(dispatchForm.expressOrderId, dispatchForm.riderId)
}
if (type === 'group' && dispatchForm.groupBuyOrderId) {
await assignGroupBuyOrder(dispatchForm.groupBuyOrderId, dispatchForm.riderId)
}
ElMessage.success('派单成功')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '派单失败')
}
}
onMounted(load)
</script>
<template>
<h1 class="page-title">配送派单</h1>
<div class="panel">
<div class="toolbar">
<strong>配送员</strong>
<el-input v-model="riderForm.name" placeholder="姓名" style="width: 140px" />
<el-input v-model="riderForm.phone" placeholder="手机号" style="width: 160px" />
<el-input-number v-model="riderForm.userId" :min="1" />
<el-button type="primary" :icon="Plus" @click="saveRider">新增</el-button>
<el-button :icon="Refresh" :loading="loading" @click="load">刷新</el-button>
</div>
<el-select v-model="dispatchForm.riderId" placeholder="选择配送员" style="width: 220px; margin-bottom: 12px">
<el-option v-for="rider in riders" :key="rider.id" :label="`${rider.name} ${rider.phone}`" :value="rider.id" />
</el-select>
<el-tabs>
<el-tab-pane label="商品订单">
<div class="toolbar">
<el-select v-model="dispatchForm.goodsOrderId" placeholder="待配送商品订单" style="width: 260px">
<el-option v-for="item in goodsOrders" :key="item.id" :label="item.orderNo" :value="item.id" />
</el-select>
<el-button type="primary" :icon="Van" @click="dispatch('goods')">派单</el-button>
</div>
</el-tab-pane>
<el-tab-pane label="快递代取">
<div class="toolbar">
<el-select v-model="dispatchForm.expressOrderId" placeholder="待取件订单" style="width: 260px">
<el-option v-for="item in expressOrders" :key="item.id" :label="`${item.orderNo} ${item.pickupCode}`" :value="item.id" />
</el-select>
<el-button type="primary" :icon="Van" @click="dispatch('express')">派单</el-button>
</div>
</el-tab-pane>
<el-tab-pane label="团购订单">
<div class="toolbar">
<el-input-number v-model="dispatchForm.groupBuyOrderId" :min="1" />
<el-button type="primary" :icon="Van" @click="dispatch('group')">派单</el-button>
</div>
</el-tab-pane>
</el-tabs>
</div>
<div class="panel">
<div class="toolbar"><strong>配送任务</strong></div>
<el-table :data="tasks" border>
<el-table-column prop="bizOrderNo" label="业务单号" min-width="150" />
<el-table-column prop="bizType" label="类型" width="150" />
<el-table-column prop="riderName" label="配送员" width="120" />
<el-table-column label="状态" width="120">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="address" label="地址" min-width="180" />
</el-table>
</div>
</template>

View File

@@ -0,0 +1,92 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Check, Edit, Money, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
adjustExpressFee,
cancelExpressOrder,
confirmExpressOrder,
listExpressOrders,
updateExpressPayStatus
} from '../api/orderApi'
import type { ExpressOrder } from '../types'
import { statusText, yuan } from '../utils/format'
const loading = ref(false)
const rows = ref<ExpressOrder[]>([])
const filters = reactive({ status: '' })
async function load() {
loading.value = true
try {
rows.value = await listExpressOrders(filters.status || undefined)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function run(action: () => Promise<ExpressOrder>) {
try {
await action()
ElMessage.success('操作成功')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
}
}
async function adjust(row: ExpressOrder) {
const { value } = await ElMessageBox.prompt('输入新的服务费(元)', '调整费用', {
inputValue: String((row.feeCent / 100).toFixed(2))
})
await run(() => adjustExpressFee(row.id, Math.round(Number(value) * 100), '后台调整'))
}
async function cancel(row: ExpressOrder) {
const { value } = await ElMessageBox.prompt('取消原因', '取消订单')
await run(() => cancelExpressOrder(row.id, value))
}
onMounted(load)
</script>
<template>
<h1 class="page-title">快递代取</h1>
<div class="panel">
<div class="toolbar">
<el-select v-model="filters.status" placeholder="订单状态" clearable style="width: 180px">
<el-option label="待确认" value="PENDING_CONFIRM" />
<el-option label="待取件" value="PENDING_PICKUP" />
<el-option label="取件中" value="PICKING_UP" />
<el-option label="配送中" value="DELIVERING" />
<el-option label="已完成" value="COMPLETED" />
</el-select>
<el-button type="primary" :icon="Search" :loading="loading" @click="load">查询</el-button>
<el-button :icon="Refresh" @click="load">刷新</el-button>
</div>
<el-table :data="rows" border v-loading="loading">
<el-table-column prop="orderNo" label="订单号" min-width="150" />
<el-table-column prop="expressCompany" label="快递公司" width="120" />
<el-table-column prop="pickupCode" label="取件码" width="120" />
<el-table-column prop="pickupAddress" label="取件地址" min-width="150" />
<el-table-column label="状态" width="120">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column label="服务费" width="110">
<template #default="{ row }">{{ yuan(row.feeCent) }}</template>
</el-table-column>
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }">
<div class="table-action">
<el-button size="small" :icon="Check" @click="run(() => confirmExpressOrder(row.id))">确认</el-button>
<el-button size="small" :icon="Edit" @click="adjust(row)">改价</el-button>
<el-button size="small" :icon="Money" @click="run(() => updateExpressPayStatus(row.id, 'PAID'))">收款</el-button>
<el-button size="small" type="danger" @click="cancel(row)">取消</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Check, Money, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {
cancelGoodsOrder,
confirmGoodsOrder,
listGoodsOrders,
markGoodsOrderPaid,
markGoodsOrderPrepared
} from '../api/orderApi'
import type { GoodsOrder } from '../types'
import { statusText, yuan } from '../utils/format'
const loading = ref(false)
const rows = ref<GoodsOrder[]>([])
const filters = reactive({ status: '' })
async function load() {
loading.value = true
try {
rows.value = await listGoodsOrders({ status: filters.status || undefined })
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function run(action: () => Promise<GoodsOrder>) {
try {
await action()
ElMessage.success('操作成功')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
}
}
onMounted(load)
</script>
<template>
<h1 class="page-title">商品订单</h1>
<div class="panel">
<div class="toolbar">
<el-select v-model="filters.status" placeholder="订单状态" clearable style="width: 180px">
<el-option label="待确认" value="PENDING_CONFIRM" />
<el-option label="备货中" value="PREPARING" />
<el-option label="待配送" value="PENDING_DELIVERY" />
<el-option label="配送中" value="DELIVERING" />
<el-option label="已完成" value="COMPLETED" />
</el-select>
<el-button type="primary" :icon="Search" :loading="loading" @click="load">查询</el-button>
<el-button :icon="Refresh" @click="load">刷新</el-button>
</div>
<el-table :data="rows" border v-loading="loading">
<el-table-column prop="orderNo" label="订单号" min-width="150" />
<el-table-column prop="merchantId" label="商户" width="90" />
<el-table-column prop="deliveryMethod" label="配送方式" width="120" />
<el-table-column label="状态" width="120">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column label="收款" width="100">
<template #default="{ row }">{{ statusText(row.offlinePayStatus) }}</template>
</el-table-column>
<el-table-column label="应收" width="110">
<template #default="{ row }">{{ yuan(row.payableAmountCent) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" />
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }">
<div class="table-action">
<el-button size="small" :icon="Check" @click="run(() => confirmGoodsOrder(row.id))">确认</el-button>
<el-button size="small" :icon="Refresh" @click="run(() => markGoodsOrderPrepared(row.id))">备好</el-button>
<el-button size="small" :icon="Money" @click="run(() => markGoodsOrderPaid(row.id))">收款</el-button>
<el-button size="small" type="danger" @click="run(() => cancelGoodsOrder(row.id))">取消</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>

View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Check, Close, Plus, Refresh, VideoPlay } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {
closeGroupBuy,
completeGroupBuy,
createGroupBuy,
listGroupBuys,
startGroupBuy
} from '../api/groupBuyApi'
import type { GroupBuy } from '../types'
import { statusText, yuan } from '../utils/format'
const loading = ref(false)
const rows = ref<GroupBuy[]>([])
const dialog = ref(false)
const form = reactive({
title: '',
coverUrl: '',
description: '',
priceCent: 0,
stock: 0,
startTime: '',
endTime: '',
pickupAddress: ''
})
async function load() {
loading.value = true
try {
rows.value = await listGroupBuys()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function save() {
try {
await createGroupBuy(form)
dialog.value = false
ElMessage.success('已创建团购')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
}
}
async function run(action: () => Promise<GroupBuy>) {
try {
await action()
ElMessage.success('操作成功')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
}
}
onMounted(load)
</script>
<template>
<h1 class="page-title">社区团购</h1>
<div class="panel">
<div class="toolbar">
<el-button type="primary" :icon="Plus" @click="dialog = true">新建团购</el-button>
<el-button :icon="Refresh" :loading="loading" @click="load">刷新</el-button>
</div>
<el-table :data="rows" border v-loading="loading">
<el-table-column prop="title" label="标题" min-width="180" />
<el-table-column label="价格" width="120">
<template #default="{ row }">{{ yuan(row.priceCent) }}</template>
</el-table-column>
<el-table-column prop="stock" label="库存" width="90" />
<el-table-column prop="soldCount" label="已售" width="90" />
<el-table-column label="状态" width="120">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="endTime" label="截单时间" min-width="170" />
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<div class="table-action">
<el-button size="small" :icon="VideoPlay" @click="run(() => startGroupBuy(row.id))">开始</el-button>
<el-button size="small" :icon="Close" @click="run(() => closeGroupBuy(row.id))">截单</el-button>
<el-button size="small" :icon="Check" @click="run(() => completeGroupBuy(row.id))">完成</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="dialog" title="新建团购" width="560px">
<el-form label-width="90px">
<el-form-item label="标题"><el-input v-model="form.title" /></el-form-item>
<el-form-item label="封面"><el-input v-model="form.coverUrl" /></el-form-item>
<el-form-item label="价格"><el-input-number v-model="form.priceCent" :min="0" /></el-form-item>
<el-form-item label="库存"><el-input-number v-model="form.stock" :min="0" /></el-form-item>
<el-form-item label="开始时间"><el-input v-model="form.startTime" placeholder="2026-07-07T08:00:00" /></el-form-item>
<el-form-item label="截单时间"><el-input v-model="form.endTime" placeholder="2026-07-07T20:00:00" /></el-form-item>
<el-form-item label="自提点"><el-input v-model="form.pickupAddress" /></el-form-item>
<el-form-item label="描述"><el-input v-model="form.description" type="textarea" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialog = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Lock, User } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { adminLogin } from '../api/authApi'
const router = useRouter()
const loading = ref(false)
const form = reactive({
username: 'admin',
password: 'admin123'
})
async function submit() {
loading.value = true
try {
const user = await adminLogin(form.username, form.password)
localStorage.setItem('admin_token', user.token)
localStorage.setItem('admin_user', JSON.stringify(user))
ElMessage.success('登录成功')
router.push('/')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '登录失败')
} finally {
loading.value = false
}
}
</script>
<template>
<div class="login-page">
<div class="login-panel">
<h1 class="login-title">邻小帮后台</h1>
<p class="login-subtitle">社区运营管理</p>
<el-form label-position="top" @submit.prevent>
<el-form-item label="账号">
<el-input v-model="form.username" :prefix-icon="User" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" show-password :prefix-icon="Lock" />
</el-form-item>
<el-button type="primary" :loading="loading" style="width: 100%" @click="submit">
登录
</el-button>
</el-form>
</div>
</div>
</template>

View File

@@ -0,0 +1,101 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Check, Close, Delete, Plus, Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {
createNotice,
deleteNotice,
listNotices,
offlineNotice,
publishNotice
} from '../api/noticeApi'
import type { Notice } from '../types'
import { statusText } from '../utils/format'
const loading = ref(false)
const rows = ref<Notice[]>([])
const dialog = ref(false)
const form = reactive({
title: '',
category: '物业通知',
content: '',
pinned: false
})
async function load() {
loading.value = true
try {
rows.value = await listNotices()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function save() {
try {
await createNotice(form)
dialog.value = false
ElMessage.success('已创建公告')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
}
}
async function run(action: () => Promise<Notice | void>) {
try {
await action()
ElMessage.success('操作成功')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
}
}
onMounted(load)
</script>
<template>
<h1 class="page-title">社区公告</h1>
<div class="panel">
<div class="toolbar">
<el-button type="primary" :icon="Plus" @click="dialog = true">新建公告</el-button>
<el-button :icon="Refresh" :loading="loading" @click="load">刷新</el-button>
</div>
<el-table :data="rows" border v-loading="loading">
<el-table-column prop="title" label="标题" min-width="180" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column label="置顶" width="90">
<template #default="{ row }">{{ row.pinned ? '是' : '否' }}</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="publishedAt" label="发布时间" min-width="160" />
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<div class="table-action">
<el-button size="small" :icon="Check" @click="run(() => publishNotice(row.id))">发布</el-button>
<el-button size="small" :icon="Close" @click="run(() => offlineNotice(row.id))">下线</el-button>
<el-button size="small" type="danger" :icon="Delete" @click="run(() => deleteNotice(row.id))">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="dialog" title="新建公告" width="560px">
<el-form label-width="80px">
<el-form-item label="标题"><el-input v-model="form.title" /></el-form-item>
<el-form-item label="分类"><el-input v-model="form.category" /></el-form-item>
<el-form-item label="置顶"><el-switch v-model="form.pinned" /></el-form-item>
<el-form-item label="内容"><el-input v-model="form.content" type="textarea" :rows="5" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialog = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</template>

View File

@@ -0,0 +1,173 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Check, Edit, Plus, Refresh, Upload } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {
addSku,
createProduct,
createProductCategory,
listProductCategories,
listProducts,
offShelfProduct,
onShelfProduct
} from '../api/productApi'
import type { Product, ProductCategory } from '../types'
import { statusText, yuan } from '../utils/format'
const loading = ref(false)
const products = ref<Product[]>([])
const categories = ref<ProductCategory[]>([])
const productDialog = ref(false)
const categoryDialog = ref(false)
const skuDialog = ref(false)
const activeProduct = ref<Product | null>(null)
const productForm = reactive({ merchantId: 1, categoryId: 0, name: '', unitName: '件', coverUrl: '', description: '' })
const categoryForm = reactive({ name: '', sortNo: 0, enabled: true })
const skuForm = reactive({ skuName: '', priceCent: 0, originPriceCent: undefined as number | undefined, stock: 0, enabled: true })
async function load() {
loading.value = true
try {
const [productRows, categoryRows] = await Promise.all([listProducts(), listProductCategories()])
products.value = productRows
categories.value = categoryRows
if (!productForm.categoryId && categoryRows[0]) {
productForm.categoryId = categoryRows[0].id
}
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function saveProduct() {
try {
await createProduct(productForm)
productDialog.value = false
ElMessage.success('已创建商品')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
}
}
async function saveCategory() {
try {
await createProductCategory(categoryForm)
categoryDialog.value = false
ElMessage.success('已创建分类')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
}
}
function openSku(row: Product) {
activeProduct.value = row
skuForm.skuName = ''
skuForm.priceCent = 0
skuForm.stock = 0
skuDialog.value = true
}
async function saveSku() {
if (!activeProduct.value) return
try {
await addSku(activeProduct.value.id, skuForm)
skuDialog.value = false
ElMessage.success('已添加规格')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
}
}
async function shelf(row: Product, onShelf: boolean) {
try {
await (onShelf ? onShelfProduct(row.id) : offShelfProduct(row.id))
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
}
}
onMounted(load)
</script>
<template>
<h1 class="page-title">商品管理</h1>
<div class="panel">
<div class="toolbar">
<el-button type="primary" :icon="Plus" @click="productDialog = true">新建商品</el-button>
<el-button :icon="Plus" @click="categoryDialog = true">新建分类</el-button>
<el-button :icon="Refresh" :loading="loading" @click="load">刷新</el-button>
</div>
<el-table :data="products" border v-loading="loading">
<el-table-column prop="name" label="商品" min-width="160" />
<el-table-column prop="merchantId" label="商户" width="90" />
<el-table-column prop="categoryId" label="分类" width="90" />
<el-table-column label="状态" width="110">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column label="规格" min-width="220">
<template #default="{ row }">
<el-tag v-for="sku in row.skus" :key="sku.id" style="margin-right: 6px">
{{ sku.skuName }} {{ yuan(sku.priceCent) }} / 库存 {{ sku.stock }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<div class="table-action">
<el-button size="small" :icon="Edit" @click="openSku(row)">规格</el-button>
<el-button size="small" :icon="Upload" @click="shelf(row, true)">上架</el-button>
<el-button size="small" :icon="Check" @click="shelf(row, false)">下架</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="productDialog" title="新建商品" width="520px">
<el-form label-width="90px">
<el-form-item label="名称"><el-input v-model="productForm.name" /></el-form-item>
<el-form-item label="商户"><el-input-number v-model="productForm.merchantId" :min="1" /></el-form-item>
<el-form-item label="分类">
<el-select v-model="productForm.categoryId" style="width: 220px">
<el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="单位"><el-input v-model="productForm.unitName" /></el-form-item>
<el-form-item label="封面"><el-input v-model="productForm.coverUrl" /></el-form-item>
<el-form-item label="描述"><el-input v-model="productForm.description" type="textarea" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="productDialog = false">取消</el-button>
<el-button type="primary" @click="saveProduct">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="categoryDialog" title="新建分类" width="420px">
<el-form label-width="80px">
<el-form-item label="名称"><el-input v-model="categoryForm.name" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="categoryForm.sortNo" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="categoryDialog = false">取消</el-button>
<el-button type="primary" @click="saveCategory">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="skuDialog" title="添加规格" width="420px">
<el-form label-width="80px">
<el-form-item label="规格"><el-input v-model="skuForm.skuName" /></el-form-item>
<el-form-item label="价格"><el-input-number v-model="skuForm.priceCent" :min="0" /></el-form-item>
<el-form-item label="库存"><el-input-number v-model="skuForm.stock" :min="0" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="skuDialog = false">取消</el-button>
<el-button type="primary" @click="saveSku">保存</el-button>
</template>
</el-dialog>
</template>

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Check, Close, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
approveSecondGoods,
listSecondGoods,
offShelfSecondGoods,
rejectSecondGoods
} from '../api/secondGoodsApi'
import type { SecondGoods } from '../types'
import { statusText, yuan } from '../utils/format'
const loading = ref(false)
const rows = ref<SecondGoods[]>([])
const filters = reactive({ status: 'PENDING_REVIEW' })
async function load() {
loading.value = true
try {
rows.value = await listSecondGoods(filters.status || undefined)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function run(action: () => Promise<SecondGoods>) {
try {
await action()
ElMessage.success('操作成功')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
}
}
async function reject(row: SecondGoods) {
const { value } = await ElMessageBox.prompt('拒绝原因', '驳回闲置')
await run(() => rejectSecondGoods(row.id, value))
}
onMounted(load)
</script>
<template>
<h1 class="page-title">闲置审核</h1>
<div class="panel">
<div class="toolbar">
<el-select v-model="filters.status" clearable placeholder="状态" style="width: 180px">
<el-option label="待审核" value="PENDING_REVIEW" />
<el-option label="已发布" value="PUBLISHED" />
<el-option label="已驳回" value="REJECTED" />
<el-option label="已下架" value="OFF_SHELF" />
</el-select>
<el-button type="primary" :icon="Search" :loading="loading" @click="load">查询</el-button>
<el-button :icon="Refresh" @click="load">刷新</el-button>
</div>
<el-table :data="rows" border v-loading="loading">
<el-table-column prop="title" label="标题" min-width="180" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column label="价格" width="110">
<template #default="{ row }">{{ yuan(row.priceCent) }}</template>
</el-table-column>
<el-table-column prop="contactPhone" label="电话" width="140" />
<el-table-column label="状态" width="120">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="reviewReason" label="原因" min-width="140" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<div class="table-action">
<el-button size="small" :icon="Check" @click="run(() => approveSecondGoods(row.id))">通过</el-button>
<el-button size="small" :icon="Close" @click="reject(row)">驳回</el-button>
<el-button size="small" type="danger" @click="run(() => offShelfSecondGoods(row.id))">下架</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>