This commit is contained in:
jiangdong
2025-10-03 11:24:11 +08:00
commit d81cf186b0
67 changed files with 10243 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FateMaster Admin - 管理后台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
{
"name": "fatemaster-admin",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"pinia": "^2.1.7",
"ant-design-vue": "^4.2.1",
"axios": "^1.6.8",
"@ant-design/icons-vue": "^7.0.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"typescript": "^5.4.3",
"vite": "^5.2.0",
"vue-tsc": "^2.0.6",
"@types/node": "^20.11.30"
}
}

View File

@@ -0,0 +1,6 @@
<template>
<router-view />
</template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,72 @@
<template>
<a-layout style="min-height: 100vh">
<a-layout-sider v-model:collapsed="collapsed" collapsible>
<div class="logo">
<h2 style="color: white; text-align: center; padding: 16px 0">Admin</h2>
</div>
<a-menu
v-model:selectedKeys="selectedKeys"
theme="dark"
mode="inline"
>
<a-menu-item key="dashboard" @click="router.push('/dashboard')">
<DashboardOutlined />
<span>仪表盘</span>
</a-menu-item>
<a-menu-item key="records" @click="router.push('/records')">
<FileTextOutlined />
<span>卜卦记录</span>
</a-menu-item>
<a-menu-item key="prices" @click="router.push('/prices')">
<DollarOutlined />
<span>价格配置</span>
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 0 24px">
<h2>FateMaster 管理后台</h2>
</a-layout-header>
<a-layout-content style="margin: 16px">
<div style="padding: 24px; background: #fff; min-height: 360px">
<router-view />
</div>
</a-layout-content>
<a-layout-footer style="text-align: center">
FateMaster Admin © 2024
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
DashboardOutlined,
FileTextOutlined,
DollarOutlined,
} from '@ant-design/icons-vue'
const router = useRouter()
const route = useRoute()
const collapsed = ref(false)
const selectedKeys = ref(['dashboard'])
watch(
() => route.path,
(path) => {
const key = path.split('/')[1] || 'dashboard'
selectedKeys.value = [key]
},
{ immediate: true }
)
</script>
<style scoped>
.logo {
height: 32px;
margin: 16px;
}
</style>

View File

@@ -0,0 +1,15 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import router from './router'
import App from './App.vue'
import 'ant-design-vue/dist/reset.css'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(Antd)
app.mount('#app')

View File

@@ -0,0 +1,34 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Layout from '@/layouts/AdminLayout.vue'
const routes: RouteRecordRaw[] = [
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
},
{
path: 'records',
name: 'Records',
component: () => import('@/views/Records.vue'),
},
{
path: 'prices',
name: 'Prices',
component: () => import('@/views/Prices.vue'),
},
],
},
]
const router = createRouter({
history: createWebHistory('/admin/'),
routes,
})
export default router

View File

@@ -0,0 +1,80 @@
<template>
<div class="dashboard">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="总订单数"
:value="statistics.total"
:loading="loading"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="已支付订单"
:value="statistics.paidCount"
:loading="loading"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="总收入"
:value="statistics.totalRevenue"
:precision="2"
prefix="¥"
:loading="loading"
/>
</a-card>
</a-col>
</a-row>
<a-card title="各类型卜卦统计" style="margin-top: 16px" :loading="loading">
<a-table
:columns="columns"
:data-source="statistics.typeStats"
:pagination="false"
/>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
const loading = ref(true)
const statistics = ref({
total: 0,
paidCount: 0,
totalRevenue: 0,
typeStats: [],
})
const columns = [
{
title: '类型',
dataIndex: 'Type',
key: 'Type',
},
{
title: '数量',
dataIndex: 'Count',
key: 'Count',
},
]
onMounted(async () => {
try {
const response = await axios.get('/api/admin/records/statistics')
statistics.value = response.data
} catch (error) {
console.error('Failed to fetch statistics:', error)
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div class="prices">
<a-table
:columns="columns"
:data-source="prices"
:loading="loading"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'Price'">
<a-input-number
v-if="editingKey === record.Id"
v-model:value="record.Price"
:min="0"
:precision="2"
/>
<span v-else>¥{{ record.Price }}</span>
</template>
<template v-else-if="column.key === 'IsEnabled'">
<a-switch
v-if="editingKey === record.Id"
v-model:checked="record.IsEnabled"
/>
<a-tag v-else :color="record.IsEnabled ? 'green' : 'red'">
{{ record.IsEnabled ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<span v-if="editingKey === record.Id">
<a @click="save(record)">保存</a>
<a-divider type="vertical" />
<a @click="cancel">取消</a>
</span>
<a v-else @click="edit(record.Id)">编辑</a>
</template>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { message } from 'ant-design-vue'
const loading = ref(false)
const prices = ref([])
const editingKey = ref<number | null>(null)
const columns = [
{ title: '服务类型', dataIndex: 'ServiceType', key: 'ServiceType' },
{ title: '价格', dataIndex: 'Price', key: 'Price' },
{ title: '货币', dataIndex: 'Currency', key: 'Currency' },
{ title: '状态', dataIndex: 'IsEnabled', key: 'IsEnabled' },
{ title: '操作', key: 'action' },
]
const fetchPrices = async () => {
loading.value = true
try {
const response = await axios.get('/api/admin/prices')
prices.value = response.data
} catch (error) {
console.error('Failed to fetch prices:', error)
} finally {
loading.value = false
}
}
const edit = (key: number) => {
editingKey.value = key
}
const cancel = () => {
editingKey.value = null
fetchPrices()
}
const save = async (record: any) => {
try {
await axios.put(`/api/admin/prices/${record.Id}`, {
Price: record.Price,
IsEnabled: record.IsEnabled,
})
message.success('保存成功')
editingKey.value = null
fetchPrices()
} catch (error) {
message.error('保存失败')
}
}
onMounted(() => {
fetchPrices()
})
</script>

View File

@@ -0,0 +1,94 @@
<template>
<div class="records">
<a-space style="margin-bottom: 16px">
<a-select
v-model:value="filters.type"
placeholder="选择类型"
style="width: 120px"
@change="fetchRecords"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="bazi">批八字</a-select-option>
<a-select-option value="career">事业</a-select-option>
<a-select-option value="marriage">姻缘</a-select-option>
<a-select-option value="tarot">塔罗</a-select-option>
<a-select-option value="zodiac">星座</a-select-option>
</a-select>
<a-select
v-model:value="filters.paymentStatus"
placeholder="支付状态"
style="width: 120px"
@change="fetchRecords"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="pending">待支付</a-select-option>
<a-select-option value="paid">已支付</a-select-option>
<a-select-option value="failed">失败</a-select-option>
</a-select>
</a-space>
<a-table
:columns="columns"
:data-source="records"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import axios from 'axios'
const loading = ref(false)
const records = ref([])
const filters = reactive({
type: '',
paymentStatus: '',
})
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
})
const columns = [
{ title: 'ID', dataIndex: 'Id', key: 'Id' },
{ title: '类型', dataIndex: 'Type', key: 'Type' },
{ title: '支付状态', dataIndex: 'PaymentStatus', key: 'PaymentStatus' },
{ title: '金额', dataIndex: 'Amount', key: 'Amount' },
{ title: '创建时间', dataIndex: 'CreatedAt', key: 'CreatedAt' },
]
const fetchRecords = async () => {
loading.value = true
try {
const response = await axios.get('/api/admin/records', {
params: {
page: pagination.current,
pageSize: pagination.pageSize,
type: filters.type || undefined,
paymentStatus: filters.paymentStatus || undefined,
},
})
records.value = response.data.data
pagination.total = response.data.total
} catch (error) {
console.error('Failed to fetch records:', error)
} finally {
loading.value = false
}
}
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchRecords()
}
onMounted(() => {
fetchRecords()
})
</script>

View File

@@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3001,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
},
},
},
})