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

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
target/ target/
*.class *.class
*.log *.log
*.tsbuildinfo
node_modules/ node_modules/
dist/ dist/

12
admin-web/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>邻小帮后台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2379
admin-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

3
admin-web/src/App.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@@ -0,0 +1,10 @@
import type { CurrentUser } from '../types'
import { apiPost } from './http'
export function adminLogin(username: string, password: string) {
return apiPost<CurrentUser>('/api/auth/admin-login', { username, password })
}
export function logout() {
return apiPost<void>('/api/auth/logout')
}

View File

@@ -0,0 +1,34 @@
import type { DeliveryOrder, DeliveryUser } from '../types'
import { apiGet, apiPost, apiPut } from './http'
export function listDeliveryUsers() {
return apiGet<DeliveryUser[]>('/api/admin/delivery-users')
}
export function createDeliveryUser(data: Partial<DeliveryUser>) {
return apiPost<DeliveryUser>('/api/admin/delivery-users', data)
}
export function enableDeliveryUser(id: number) {
return apiPut<DeliveryUser>(`/api/admin/delivery-users/${id}/enable`)
}
export function disableDeliveryUser(id: number) {
return apiPut<DeliveryUser>(`/api/admin/delivery-users/${id}/disable`)
}
export function listDeliveryOrders(status?: string) {
return apiGet<DeliveryOrder[]>('/api/admin/delivery-orders', status ? { status } : undefined)
}
export function assignGoodsOrder(orderId: number, riderId: number) {
return apiPost<DeliveryOrder>('/api/admin/delivery-orders/assign-goods-order', { orderId, riderId })
}
export function assignExpressOrder(orderId: number, riderId: number) {
return apiPost<DeliveryOrder>('/api/admin/delivery-orders/assign-express-order', { orderId, riderId })
}
export function assignGroupBuyOrder(orderId: number, riderId: number) {
return apiPost<DeliveryOrder>('/api/admin/delivery-orders/assign-group-buy-order', { orderId, riderId })
}

View File

@@ -0,0 +1,26 @@
import type { GroupBuy } from '../types'
import { apiGet, apiPost, apiPut } from './http'
export function listGroupBuys() {
return apiGet<GroupBuy[]>('/api/admin/group-buys')
}
export function createGroupBuy(data: Partial<GroupBuy>) {
return apiPost<GroupBuy>('/api/admin/group-buys', data)
}
export function updateGroupBuy(id: number, data: Partial<GroupBuy>) {
return apiPut<GroupBuy>(`/api/admin/group-buys/${id}`, data)
}
export function startGroupBuy(id: number) {
return apiPut<GroupBuy>(`/api/admin/group-buys/${id}/start`)
}
export function closeGroupBuy(id: number) {
return apiPut<GroupBuy>(`/api/admin/group-buys/${id}/close`)
}
export function completeGroupBuy(id: number) {
return apiPut<GroupBuy>(`/api/admin/group-buys/${id}/complete`)
}

46
admin-web/src/api/http.ts Normal file
View File

@@ -0,0 +1,46 @@
import axios, { AxiosHeaders } from 'axios'
export interface ApiEnvelope<T> {
code: number
message: string
data: T
}
export const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || ''
})
http.interceptors.request.use((config) => {
const token = localStorage.getItem('admin_token')
if (token) {
const headers = AxiosHeaders.from(config.headers)
headers.set('satoken', token)
headers.set('Authorization', token)
config.headers = headers
}
return config
})
async function unwrap<T>(request: Promise<{ data: ApiEnvelope<T> }>): Promise<T> {
const response = await request
if (response.data.code !== 0) {
throw new Error(response.data.message || '请求失败')
}
return response.data.data
}
export function apiGet<T>(url: string, params?: Record<string, unknown>) {
return unwrap<T>(http.get(url, { params }))
}
export function apiPost<T>(url: string, data?: unknown) {
return unwrap<T>(http.post(url, data))
}
export function apiPut<T>(url: string, data?: unknown) {
return unwrap<T>(http.put(url, data))
}
export function apiDelete<T>(url: string) {
return unwrap<T>(http.delete(url))
}

View File

@@ -0,0 +1,26 @@
import type { Notice } from '../types'
import { apiDelete, apiGet, apiPost, apiPut } from './http'
export function listNotices() {
return apiGet<Notice[]>('/api/admin/notices')
}
export function createNotice(data: Partial<Notice>) {
return apiPost<Notice>('/api/admin/notices', data)
}
export function updateNotice(id: number, data: Partial<Notice>) {
return apiPut<Notice>(`/api/admin/notices/${id}`, data)
}
export function publishNotice(id: number) {
return apiPut<Notice>(`/api/admin/notices/${id}/publish`)
}
export function offlineNotice(id: number) {
return apiPut<Notice>(`/api/admin/notices/${id}/offline`)
}
export function deleteNotice(id: number) {
return apiDelete<void>(`/api/admin/notices/${id}`)
}

View File

@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest'
import { buildGoodsOrderQuery } from './orderApi'
describe('buildGoodsOrderQuery', () => {
it('keeps status and date range filters stable', () => {
expect(buildGoodsOrderQuery({
status: 'PENDING_DELIVERY',
startDate: '2026-07-01',
endDate: '2026-07-07'
})).toEqual({
status: 'PENDING_DELIVERY',
startDate: '2026-07-01',
endDate: '2026-07-07'
})
})
})

View File

@@ -0,0 +1,56 @@
import type { ExpressOrder, GoodsOrder } from '../types'
import { apiGet, apiPut } from './http'
export interface GoodsOrderQuery {
status?: string
startDate?: string
endDate?: string
}
export function buildGoodsOrderQuery(query: GoodsOrderQuery) {
const params: Record<string, string> = {}
if (query.status) params.status = query.status
if (query.startDate) params.startDate = query.startDate
if (query.endDate) params.endDate = query.endDate
return params
}
export function listGoodsOrders(query: GoodsOrderQuery = {}) {
return apiGet<GoodsOrder[]>('/api/admin/goods-orders', buildGoodsOrderQuery(query))
}
export function confirmGoodsOrder(id: number) {
return apiPut<GoodsOrder>(`/api/admin/goods-orders/${id}/confirm`)
}
export function markGoodsOrderPrepared(id: number) {
return apiPut<GoodsOrder>(`/api/admin/goods-orders/${id}/prepared`)
}
export function cancelGoodsOrder(id: number) {
return apiPut<GoodsOrder>(`/api/mini/goods-orders/${id}/cancel`)
}
export function markGoodsOrderPaid(id: number) {
return apiPut<GoodsOrder>(`/api/admin/goods-orders/${id}/paid`)
}
export function listExpressOrders(status?: string) {
return apiGet<ExpressOrder[]>('/api/admin/express-orders', status ? { status } : undefined)
}
export function confirmExpressOrder(id: number) {
return apiPut<ExpressOrder>(`/api/admin/express-orders/${id}/confirm`)
}
export function adjustExpressFee(id: number, feeCent: number, reason: string) {
return apiPut<ExpressOrder>(`/api/admin/express-orders/${id}/adjust-fee`, { feeCent, reason })
}
export function cancelExpressOrder(id: number, reason?: string) {
return apiPut<ExpressOrder>(`/api/admin/express-orders/${id}/cancel`, { reason })
}
export function updateExpressPayStatus(id: number, status: string) {
return apiPut<ExpressOrder>(`/api/admin/express-orders/${id}/offline-pay-status`, { status })
}

View File

@@ -0,0 +1,38 @@
import type { Product, ProductCategory, ProductSku } from '../types'
import { apiGet, apiPost, apiPut } from './http'
export function listProducts() {
return apiGet<Product[]>('/api/admin/products')
}
export function createProduct(data: Partial<Product>) {
return apiPost<Product>('/api/admin/products', data)
}
export function updateProduct(id: number, data: Partial<Product>) {
return apiPut<Product>(`/api/admin/products/${id}`, data)
}
export function onShelfProduct(id: number) {
return apiPut<Product>(`/api/admin/products/${id}/on-shelf`)
}
export function offShelfProduct(id: number) {
return apiPut<Product>(`/api/admin/products/${id}/off-shelf`)
}
export function listProductCategories() {
return apiGet<ProductCategory[]>('/api/admin/product-categories')
}
export function createProductCategory(data: Partial<ProductCategory>) {
return apiPost<ProductCategory>('/api/admin/product-categories', data)
}
export function addSku(productId: number, data: Partial<ProductSku>) {
return apiPost<ProductSku>(`/api/admin/products/${productId}/skus`, data)
}
export function updateSku(productId: number, skuId: number, data: Partial<ProductSku>) {
return apiPut<ProductSku>(`/api/admin/products/${productId}/skus/${skuId}`, data)
}

View File

@@ -0,0 +1,18 @@
import type { SecondGoods } from '../types'
import { apiGet, apiPut } from './http'
export function listSecondGoods(status?: string) {
return apiGet<SecondGoods[]>('/api/admin/second-goods', status ? { status } : undefined)
}
export function approveSecondGoods(id: number) {
return apiPut<SecondGoods>(`/api/admin/second-goods/${id}/approve`)
}
export function rejectSecondGoods(id: number, reason: string) {
return apiPut<SecondGoods>(`/api/admin/second-goods/${id}/reject`, { reason })
}
export function offShelfSecondGoods(id: number) {
return apiPut<SecondGoods>(`/api/admin/second-goods/${id}/off-shelf`)
}

8
admin-web/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
export default component
}

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
Bell,
Box,
DataLine,
Goods,
House,
List,
Shop,
SwitchButton,
Tickets,
Van,
User
} from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
const navItems = [
{ path: '/', label: '仪表盘', icon: DataLine },
{ path: '/products', label: '商品管理', icon: Goods },
{ path: '/goods-orders', label: '商品订单', icon: List },
{ path: '/express-orders', label: '快递代取', icon: Box },
{ path: '/dispatch', label: '配送派单', icon: Van },
{ path: '/group-buys', label: '社区团购', icon: Shop },
{ path: '/second-goods', label: '闲置审核', icon: Tickets },
{ path: '/notices', label: '社区公告', icon: Bell }
]
const currentUser = computed(() => {
try {
return JSON.parse(localStorage.getItem('admin_user') || '{}')
} catch {
return {}
}
})
function signOut() {
localStorage.removeItem('admin_token')
localStorage.removeItem('admin_user')
router.push('/login')
}
</script>
<template>
<div class="admin-shell">
<aside class="sidebar">
<div class="brand">
<el-icon><House /></el-icon>
<span style="margin-left: 8px">邻小帮后台</span>
</div>
<el-menu
class="side-menu"
:default-active="route.path"
router
background-color="#1f2933"
text-color="#dbe4ee"
active-text-color="#ffffff"
>
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</el-menu-item>
</el-menu>
</aside>
<main class="main-shell">
<header class="topbar">
<div>社区 ID{{ currentUser.communityId || 1 }}</div>
<el-button :icon="SwitchButton" @click="signOut">退出</el-button>
</header>
<section class="content">
<router-view />
</section>
</main>
</div>
</template>

8
admin-web/src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './styles.css'
import App from './App.vue'
import { router } from './router'
createApp(App).use(router).use(ElementPlus).mount('#app')

View File

@@ -0,0 +1,42 @@
import { createRouter, createWebHistory } from 'vue-router'
import AdminLayout from '../layout/AdminLayout.vue'
import LoginView from '../views/LoginView.vue'
import DashboardView from '../views/DashboardView.vue'
import ProductListView from '../views/ProductListView.vue'
import GoodsOrderListView from '../views/GoodsOrderListView.vue'
import ExpressOrderListView from '../views/ExpressOrderListView.vue'
import DeliveryDispatchView from '../views/DeliveryDispatchView.vue'
import GroupBuyListView from '../views/GroupBuyListView.vue'
import SecondGoodsReviewView from '../views/SecondGoodsReviewView.vue'
import NoticeListView from '../views/NoticeListView.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: LoginView },
{
path: '/',
component: AdminLayout,
children: [
{ path: '', name: 'dashboard', component: DashboardView },
{ path: 'products', name: 'products', component: ProductListView },
{ path: 'goods-orders', name: 'goods-orders', component: GoodsOrderListView },
{ path: 'express-orders', name: 'express-orders', component: ExpressOrderListView },
{ path: 'dispatch', name: 'dispatch', component: DeliveryDispatchView },
{ path: 'group-buys', name: 'group-buys', component: GroupBuyListView },
{ path: 'second-goods', name: 'second-goods', component: SecondGoodsReviewView },
{ path: 'notices', name: 'notices', component: NoticeListView }
]
}
]
})
router.beforeEach((to) => {
if (to.path !== '/login' && !localStorage.getItem('admin_token')) {
return '/login'
}
if (to.path === '/login' && localStorage.getItem('admin_token')) {
return '/'
}
return true
})

142
admin-web/src/styles.css Normal file
View File

@@ -0,0 +1,142 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 1180px;
color: #263238;
background: #f4f6f8;
font-family: Inter, "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
}
#app {
min-height: 100vh;
}
.admin-shell {
display: grid;
grid-template-columns: 224px 1fr;
min-height: 100vh;
}
.sidebar {
background: #1f2933;
color: #f7fafc;
display: flex;
flex-direction: column;
}
.brand {
height: 64px;
display: flex;
align-items: center;
padding: 0 20px;
font-size: 20px;
font-weight: 700;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.side-menu {
border-right: none;
}
.main-shell {
min-width: 0;
display: flex;
flex-direction: column;
}
.topbar {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: #ffffff;
border-bottom: 1px solid #e5e9ef;
}
.content {
padding: 20px 24px 32px;
}
.page-title {
margin: 0 0 16px;
font-size: 20px;
font-weight: 700;
}
.panel {
background: #ffffff;
border: 1px solid #e5e9ef;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.toolbar-spacer {
flex: 1;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(5, minmax(150px, 1fr));
gap: 12px;
}
.metric {
background: #ffffff;
border: 1px solid #e5e9ef;
border-radius: 6px;
padding: 14px;
}
.metric-label {
color: #637381;
font-size: 13px;
}
.metric-value {
margin-top: 8px;
font-size: 24px;
font-weight: 700;
}
.login-page {
min-height: 100vh;
display: grid;
place-items: center;
background: #eef2f5;
}
.login-panel {
width: 360px;
background: #ffffff;
border: 1px solid #dde3ea;
border-radius: 6px;
padding: 28px;
}
.login-title {
margin: 0 0 6px;
font-size: 24px;
}
.login-subtitle {
margin: 0 0 24px;
color: #637381;
}
.table-action {
display: inline-flex;
gap: 8px;
}

124
admin-web/src/types.ts Normal file
View File

@@ -0,0 +1,124 @@
export interface CurrentUser {
token: string
userId: number
tenantId: number
communityId: number
roleCode: string
}
export interface ProductSku {
id: number
productId: number
productName: string
skuName: string
priceCent: number
originPriceCent?: number
stock: number
enabled: boolean
}
export interface Product {
id: number
merchantId: number
categoryId: number
name: string
coverUrl?: string
description?: string
unitName: string
status: string
skus: ProductSku[]
}
export interface ProductCategory {
id: number
name: string
sortNo: number
enabled: boolean
}
export interface GoodsOrder {
id: number
orderNo: string
merchantId: number
userId: number
deliveryMethod: string
status: string
offlinePayStatus: string
totalAmountCent: number
deliveryFeeCent: number
payableAmountCent: number
remark?: string
createdAt?: string
}
export interface ExpressOrder {
id: number
orderNo: string
expressCompany: string
pickupCode: string
pickupAddress: string
contactName?: string
contactPhone?: string
deliveryAddress?: string
packageCount: number
feeCent: number
offlinePayStatus: string
status: string
remark?: string
createdAt?: string
}
export interface DeliveryUser {
id: number
userId: number
name: string
phone: string
enabled: boolean
}
export interface DeliveryOrder {
id: number
bizType: string
bizOrderId: number
bizOrderNo: string
riderId: number
riderName: string
address?: string
amountCent: number
status: string
}
export interface GroupBuy {
id: number
title: string
coverUrl?: string
priceCent: number
stock: number
soldCount: number
status: string
startTime: string
endTime: string
pickupAddress?: string
}
export interface SecondGoods {
id: number
title: string
priceCent: number
category?: string
contactPhone?: string
tradeMethod?: string
status: string
reviewReason?: string
createdAt?: string
}
export interface Notice {
id: number
title: string
category: string
content: string
pinned: boolean
status: string
publishedAt?: string
}

View File

@@ -0,0 +1,27 @@
export function yuan(value?: number) {
return `¥${((value || 0) / 100).toFixed(2)}`
}
export function statusText(value?: string) {
const map: Record<string, string> = {
PENDING_CONFIRM: '待确认',
PREPARING: '备货中',
PENDING_DELIVERY: '待配送',
PENDING_PICKUP: '待自提',
DELIVERING: '配送中',
DELIVERED: '已送达',
COMPLETED: '已完成',
CANCELED: '已取消',
UNPAID: '未收款',
PAID: '已收款',
DRAFT: '草稿',
ONGOING: '进行中',
CLOSED: '已截单',
PUBLISHED: '已发布',
PENDING_REVIEW: '待审核',
REJECTED: '已驳回',
OFF_SHELF: '已下架',
SOLD: '已售出'
}
return value ? map[value] || value : '-'
}

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>

18
admin-web/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": false,
"baseUrl": "."
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}