111
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { CandlestickChart, CircleDollarSign, Coins, DollarSign, Palette, Receipt, Settings, ShieldCheck, TrendingUp, Users } from 'lucide-vue-next'
|
||||
import { CircleDollarSign, Coins, DollarSign, Palette, Receipt, Settings, ShieldCheck, TrendingUp, Users } from 'lucide-vue-next'
|
||||
|
||||
import type { NavGroup } from '@/components/app-sidebar/types'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -22,7 +22,6 @@ export function useSidebar() {
|
||||
{ title: '订单审批', url: '/monisuo/orders', icon: Receipt, roles: [1, 2] },
|
||||
{ title: '财务审批', url: '/monisuo/finance-orders', icon: CircleDollarSign, roles: [1, 3] },
|
||||
{ title: '业务分析', url: '/monisuo/analytics', icon: TrendingUp, roles: [1] },
|
||||
{ title: 'K线配置', url: '/monisuo/kline-config', icon: CandlestickChart, roles: [1] },
|
||||
{ title: '管理员管理', url: '/monisuo/admins', icon: ShieldCheck, roles: [1] },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import {
|
||||
getKlineConfigs,
|
||||
saveKlineConfig,
|
||||
getKlinePreview,
|
||||
type KlineConfig,
|
||||
} from '../../services/api/monisuo-kline.api.ts'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CandlestickChart } from 'echarts/charts'
|
||||
import { GridComponent, TooltipComponent, LegendComponent, DataZoomComponent } from 'echarts/components'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
use([CandlestickChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, CanvasRenderer])
|
||||
|
||||
// ==================== State ====================
|
||||
const configs = ref<KlineConfig[]>([])
|
||||
const isLoading = ref(false)
|
||||
const editingConfig = ref<KlineConfig | null>(null)
|
||||
const showEditDialog = ref(false)
|
||||
const showPreviewDialog = ref(false)
|
||||
const previewCoinCode = ref('')
|
||||
const previewInterval = ref('1h')
|
||||
const previewData = ref<any[]>([])
|
||||
const previewVolumes = ref<number[]>([])
|
||||
const previewDates = ref<string[]>([])
|
||||
|
||||
// ==================== Load ====================
|
||||
async function loadConfigs() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
configs.value = await getKlineConfigs()
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(`加载失败: ${e.message}`)
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadConfigs)
|
||||
|
||||
// ==================== Edit ====================
|
||||
function openEdit(config: KlineConfig) {
|
||||
editingConfig.value = { ...config }
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editingConfig.value) return
|
||||
try {
|
||||
await saveKlineConfig({
|
||||
coinCode: editingConfig.value.coinCode,
|
||||
tradeStartTime: editingConfig.value.tradeStartTime,
|
||||
tradeEndTime: editingConfig.value.tradeEndTime,
|
||||
priceMin: editingConfig.value.priceMin,
|
||||
priceMax: editingConfig.value.priceMax,
|
||||
simulationEnabled: editingConfig.value.simulationEnabled,
|
||||
})
|
||||
toast.success('保存成功')
|
||||
showEditDialog.value = false
|
||||
loadConfigs()
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(`保存失败: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSimulation(config: KlineConfig) {
|
||||
const newConfig = { ...config }
|
||||
newConfig.simulationEnabled = config.simulationEnabled === 1 ? 0 : 1
|
||||
saveKlineConfig({
|
||||
coinCode: newConfig.coinCode,
|
||||
simulationEnabled: newConfig.simulationEnabled,
|
||||
}).then(() => {
|
||||
toast.success(newConfig.simulationEnabled ? '已启用模拟' : '已关闭模拟')
|
||||
loadConfigs()
|
||||
}).catch((e: any) => {
|
||||
toast.error(`操作失败: ${e.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Preview ====================
|
||||
async function openPreview(config: KlineConfig) {
|
||||
previewCoinCode.value = config.coinCode
|
||||
previewInterval.value = '1h'
|
||||
showPreviewDialog.value = true
|
||||
await loadPreviewData()
|
||||
}
|
||||
|
||||
async function loadPreviewData() {
|
||||
try {
|
||||
const data = await getKlinePreview(previewCoinCode.value, previewInterval.value, 100)
|
||||
previewData.value = data.map((d) => [d.openPrice, d.closePrice, d.lowPrice, d.highPrice])
|
||||
previewVolumes.value = data.map(d => d.volume)
|
||||
previewDates.value = data.map((d) => {
|
||||
const date = new Date(d.openTime)
|
||||
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
})
|
||||
}
|
||||
catch {
|
||||
previewData.value = []
|
||||
}
|
||||
}
|
||||
|
||||
watch(previewInterval, loadPreviewData)
|
||||
|
||||
const chartOption = computed(() => ({
|
||||
animation: false,
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
|
||||
grid: [
|
||||
{ left: '10%', right: '5%', top: '5%', height: '55%' },
|
||||
{ left: '10%', right: '5%', top: '68%', height: '20%' },
|
||||
],
|
||||
xAxis: [
|
||||
{ type: 'category', data: previewDates.value, gridIndex: 0, axisLabel: { show: false } },
|
||||
{ type: 'category', data: previewDates.value, gridIndex: 1 },
|
||||
],
|
||||
yAxis: [
|
||||
{ type: 'value', gridIndex: 0, scale: true },
|
||||
{ type: 'value', gridIndex: 1, scale: true },
|
||||
],
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: [0, 1], start: 50, end: 100 },
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'K线',
|
||||
type: 'candlestick',
|
||||
data: previewData.value,
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: '#ef5350',
|
||||
color0: '#26a69a',
|
||||
borderColor: '#ef5350',
|
||||
borderColor0: '#26a69a',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '成交量',
|
||||
type: 'bar',
|
||||
data: previewVolumes.value,
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
itemStyle: { color: '#7986cb' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">K线配置</h1>
|
||||
<p class="text-muted-foreground mt-1">管理币种K线模拟参数,配置交易时段和价格区间</p>
|
||||
</div>
|
||||
|
||||
<!-- 币种配置表格 -->
|
||||
<div class="border rounded-lg">
|
||||
<table class="w-full">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-muted-foreground">币种</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-muted-foreground">当前价格</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-medium text-muted-foreground">模拟状态</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-medium text-muted-foreground">交易时段</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-medium text-muted-foreground">价格区间</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-medium text-muted-foreground">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="config in configs" :key="config.coinCode" class="border-t hover:bg-muted/30">
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-medium">{{ config.coinCode }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ config.coinName }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono">{{ config.currentPrice?.toFixed(4) ?? '-' }}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<button
|
||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||
:class="config.simulationEnabled ? 'bg-primary' : 'bg-muted'"
|
||||
@click="toggleSimulation(config)"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-transform"
|
||||
:class="config.simulationEnabled ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-sm">
|
||||
{{ config.tradeStartTime ?? '09:00' }} - {{ config.tradeEndTime ?? '23:00' }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-sm font-mono">
|
||||
<template v-if="config.priceMin != null && config.priceMax != null">
|
||||
{{ config.priceMin }} ~ {{ config.priceMax }}
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button
|
||||
class="text-sm text-primary hover:underline"
|
||||
@click="openEdit(config)"
|
||||
>
|
||||
配置
|
||||
</button>
|
||||
<button
|
||||
class="text-sm text-primary hover:underline"
|
||||
@click="openPreview(config)"
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="configs.length === 0">
|
||||
<td colspan="6" class="px-4 py-8 text-center text-muted-foreground">
|
||||
暂无币种数据
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<UiDialog v-model:open="showEditDialog">
|
||||
<UiDialogContent class="max-w-md">
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>K线配置 — {{ editingConfig?.coinCode }}</UiDialogTitle>
|
||||
</UiDialogHeader>
|
||||
<div v-if="editingConfig" class="space-y-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm font-medium">启用模拟</label>
|
||||
<button
|
||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||
:class="editingConfig.simulationEnabled ? 'bg-primary' : 'bg-muted'"
|
||||
@click="editingConfig.simulationEnabled = editingConfig.simulationEnabled === 1 ? 0 : 1"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-transform"
|
||||
:class="editingConfig.simulationEnabled ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium">交易开始时间</label>
|
||||
<input
|
||||
v-model="editingConfig.tradeStartTime"
|
||||
type="time"
|
||||
class="mt-1 w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium">交易结束时间</label>
|
||||
<input
|
||||
v-model="editingConfig.tradeEndTime"
|
||||
type="time"
|
||||
class="mt-1 w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium">最低价</label>
|
||||
<input
|
||||
v-model.number="editingConfig.priceMin"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
min="0"
|
||||
class="mt-1 w-full rounded-md border px-3 py-2 text-sm"
|
||||
placeholder="最低价"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium">最高价</label>
|
||||
<input
|
||||
v-model.number="editingConfig.priceMax"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
min="0"
|
||||
class="mt-1 w-full rounded-md border px-3 py-2 text-sm"
|
||||
placeholder="最高价"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiDialogFooter>
|
||||
<UiButton variant="outline" @click="showEditDialog = false">取消</UiButton>
|
||||
<UiButton @click="saveEdit">保存</UiButton>
|
||||
</UiDialogFooter>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
|
||||
<!-- K线预览弹窗 -->
|
||||
<UiDialog v-model:open="showPreviewDialog">
|
||||
<UiDialogContent class="max-w-3xl max-h-[80vh]">
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>K线预览 — {{ previewCoinCode }}</UiDialogTitle>
|
||||
</UiDialogHeader>
|
||||
<div class="space-y-3">
|
||||
<select
|
||||
v-model="previewInterval"
|
||||
class="rounded-md border px-3 py-1.5 text-sm"
|
||||
>
|
||||
<option value="15m">15分钟</option>
|
||||
<option value="1h">1小时</option>
|
||||
<option value="4h">4小时</option>
|
||||
<option value="1d">日线</option>
|
||||
<option value="1M">月线</option>
|
||||
</select>
|
||||
<VChart v-if="previewData.length" :option="chartOption" autoresize style="height: 400px" />
|
||||
<div v-else class="py-8 text-center text-muted-foreground">暂无数据</div>
|
||||
</div>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useAxios } from '../../composables/use-axios'
|
||||
|
||||
// ==================== Types ====================
|
||||
|
||||
export interface KlineConfig {
|
||||
coinId: number
|
||||
coinCode: string
|
||||
coinName: string
|
||||
simulationEnabled: number // 0 or 1
|
||||
tradeStartTime: string // HH:mm
|
||||
tradeEndTime: string // HH:mm
|
||||
priceMin: number
|
||||
priceMax: number
|
||||
currentPrice: number
|
||||
priceType: number
|
||||
}
|
||||
|
||||
export interface KlineConfigUpdate {
|
||||
coinCode: string
|
||||
tradeStartTime?: string
|
||||
tradeEndTime?: string
|
||||
priceMin?: number
|
||||
priceMax?: number
|
||||
simulationEnabled?: number
|
||||
}
|
||||
|
||||
export interface KlineCandle {
|
||||
coinCode: string
|
||||
interval: string
|
||||
openTime: number
|
||||
openPrice: number
|
||||
highPrice: number
|
||||
lowPrice: number
|
||||
closePrice: number
|
||||
volume: number
|
||||
closeTime: number
|
||||
}
|
||||
|
||||
// ==================== API Functions ====================
|
||||
|
||||
/** 获取所有币种K线配置 */
|
||||
export async function getKlineConfigs(): Promise<KlineConfig[]> {
|
||||
const { axiosInstance } = useAxios()
|
||||
const { data } = await axiosInstance.get('/admin/kline/config')
|
||||
const list: any[] = (data as any)?.data ?? []
|
||||
return list.map(item => ({
|
||||
coinId: item.id,
|
||||
coinCode: item.code,
|
||||
coinName: item.name,
|
||||
simulationEnabled: item.simulationEnabled ?? 0,
|
||||
tradeStartTime: item.tradeStartTime,
|
||||
tradeEndTime: item.tradeEndTime,
|
||||
priceMin: item.priceMin,
|
||||
priceMax: item.priceMax,
|
||||
currentPrice: item.price,
|
||||
priceType: item.priceType,
|
||||
}))
|
||||
}
|
||||
|
||||
/** 保存K线配置 */
|
||||
export async function saveKlineConfig(config: KlineConfigUpdate): Promise<void> {
|
||||
const { axiosInstance } = useAxios()
|
||||
await axiosInstance.post('/admin/kline/config', config)
|
||||
}
|
||||
|
||||
/** 获取K线预览数据(用于 echarts) */
|
||||
export async function getKlinePreview(coinCode: string, interval: string = '1h', limit: number = 100): Promise<KlineCandle[]> {
|
||||
const { axiosInstance } = useAxios()
|
||||
const { data } = await axiosInstance.get('/admin/kline/preview', { params: { coinCode, interval, limit } })
|
||||
return (data as any)?.data ?? []
|
||||
}
|
||||
13
monisuo-admin/src/types/route-map.d.ts
vendored
13
monisuo-admin/src/types/route-map.d.ts
vendored
@@ -96,13 +96,6 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/monisuo/kline-config': RouteRecordInfo<
|
||||
'/monisuo/kline-config',
|
||||
'/monisuo/kline-config',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/monisuo/orders': RouteRecordInfo<
|
||||
'/monisuo/orders',
|
||||
'/monisuo/orders',
|
||||
@@ -219,12 +212,6 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/monisuo/kline-config.vue': {
|
||||
routes:
|
||||
| '/monisuo/kline-config'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/monisuo/orders.vue': {
|
||||
routes:
|
||||
| '/monisuo/orders'
|
||||
|
||||
Reference in New Issue
Block a user