feat: add admin web MVP
This commit is contained in:
12
admin-web/index.html
Normal file
12
admin-web/index.html
Normal 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
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
3
admin-web/src/App.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
10
admin-web/src/api/authApi.ts
Normal file
10
admin-web/src/api/authApi.ts
Normal 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')
|
||||
}
|
||||
34
admin-web/src/api/deliveryApi.ts
Normal file
34
admin-web/src/api/deliveryApi.ts
Normal 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 })
|
||||
}
|
||||
26
admin-web/src/api/groupBuyApi.ts
Normal file
26
admin-web/src/api/groupBuyApi.ts
Normal 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
46
admin-web/src/api/http.ts
Normal 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))
|
||||
}
|
||||
26
admin-web/src/api/noticeApi.ts
Normal file
26
admin-web/src/api/noticeApi.ts
Normal 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}`)
|
||||
}
|
||||
16
admin-web/src/api/orderApi.test.ts
Normal file
16
admin-web/src/api/orderApi.test.ts
Normal 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'
|
||||
})
|
||||
})
|
||||
})
|
||||
56
admin-web/src/api/orderApi.ts
Normal file
56
admin-web/src/api/orderApi.ts
Normal 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 })
|
||||
}
|
||||
38
admin-web/src/api/productApi.ts
Normal file
38
admin-web/src/api/productApi.ts
Normal 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)
|
||||
}
|
||||
18
admin-web/src/api/secondGoodsApi.ts
Normal file
18
admin-web/src/api/secondGoodsApi.ts
Normal 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
8
admin-web/src/env.d.ts
vendored
Normal 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
|
||||
}
|
||||
78
admin-web/src/layout/AdminLayout.vue
Normal file
78
admin-web/src/layout/AdminLayout.vue
Normal 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
8
admin-web/src/main.ts
Normal 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')
|
||||
42
admin-web/src/router/index.ts
Normal file
42
admin-web/src/router/index.ts
Normal 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
142
admin-web/src/styles.css
Normal 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
124
admin-web/src/types.ts
Normal 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
|
||||
}
|
||||
27
admin-web/src/utils/format.ts
Normal file
27
admin-web/src/utils/format.ts
Normal 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 : '-'
|
||||
}
|
||||
93
admin-web/src/views/DashboardView.vue
Normal file
93
admin-web/src/views/DashboardView.vue
Normal 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>
|
||||
134
admin-web/src/views/DeliveryDispatchView.vue
Normal file
134
admin-web/src/views/DeliveryDispatchView.vue
Normal 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>
|
||||
92
admin-web/src/views/ExpressOrderListView.vue
Normal file
92
admin-web/src/views/ExpressOrderListView.vue
Normal 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>
|
||||
83
admin-web/src/views/GoodsOrderListView.vue
Normal file
83
admin-web/src/views/GoodsOrderListView.vue
Normal 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>
|
||||
110
admin-web/src/views/GroupBuyListView.vue
Normal file
110
admin-web/src/views/GroupBuyListView.vue
Normal 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>
|
||||
49
admin-web/src/views/LoginView.vue
Normal file
49
admin-web/src/views/LoginView.vue
Normal 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>
|
||||
101
admin-web/src/views/NoticeListView.vue
Normal file
101
admin-web/src/views/NoticeListView.vue
Normal 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>
|
||||
173
admin-web/src/views/ProductListView.vue
Normal file
173
admin-web/src/views/ProductListView.vue
Normal 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>
|
||||
82
admin-web/src/views/SecondGoodsReviewView.vue
Normal file
82
admin-web/src/views/SecondGoodsReviewView.vue
Normal 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
18
admin-web/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user