init
This commit is contained in:
13
frontend/fatemaster-admin/index.html
Normal file
13
frontend/fatemaster-admin/index.html
Normal 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>
|
||||
26
frontend/fatemaster-admin/package.json
Normal file
26
frontend/fatemaster-admin/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
frontend/fatemaster-admin/src/App.vue
Normal file
6
frontend/fatemaster-admin/src/App.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
72
frontend/fatemaster-admin/src/layouts/AdminLayout.vue
Normal file
72
frontend/fatemaster-admin/src/layouts/AdminLayout.vue
Normal 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>
|
||||
15
frontend/fatemaster-admin/src/main.ts
Normal file
15
frontend/fatemaster-admin/src/main.ts
Normal 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')
|
||||
34
frontend/fatemaster-admin/src/router/index.ts
Normal file
34
frontend/fatemaster-admin/src/router/index.ts
Normal 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
|
||||
80
frontend/fatemaster-admin/src/views/Dashboard.vue
Normal file
80
frontend/fatemaster-admin/src/views/Dashboard.vue
Normal 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>
|
||||
96
frontend/fatemaster-admin/src/views/Prices.vue
Normal file
96
frontend/fatemaster-admin/src/views/Prices.vue
Normal 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>
|
||||
94
frontend/fatemaster-admin/src/views/Records.vue
Normal file
94
frontend/fatemaster-admin/src/views/Records.vue
Normal 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>
|
||||
21
frontend/fatemaster-admin/vite.config.ts
Normal file
21
frontend/fatemaster-admin/vite.config.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user