.
This commit is contained in:
96
web/src/views/auth/ActivateForm.vue
Normal file
96
web/src/views/auth/ActivateForm.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<a-form :model="formData" layout="vertical" @submit="handleSubmit">
|
||||
<a-form-item
|
||||
label="授权码"
|
||||
:validate-status="error ? 'error' : ''"
|
||||
:help="error || '请输入授权码(至少16位字母和数字)'"
|
||||
>
|
||||
<a-input
|
||||
v-model="formData.licenseCode"
|
||||
placeholder="请输入授权码"
|
||||
:max-length="100"
|
||||
allow-clear
|
||||
@input="handleInput"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSubmit" :loading="loading">
|
||||
激活
|
||||
</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { ActivateLicense } from '../../wailsjs/go/main/App'
|
||||
|
||||
const emit = defineEmits(['activated', 'reset'])
|
||||
|
||||
const formData = ref({
|
||||
licenseCode: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const handleInput = () => {
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.value.licenseCode || formData.value.licenseCode.trim() === '') {
|
||||
error.value = '授权码不能为空'
|
||||
return
|
||||
}
|
||||
|
||||
const cleaned = formData.value.licenseCode.replace(/[\s-]/g, '')
|
||||
if (cleaned.length < 16) {
|
||||
error.value = '授权码长度不足,至少需要16位字符'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const result = await ActivateLicense(formData.value.licenseCode)
|
||||
|
||||
if (result.success) {
|
||||
Message.success(result.message || '激活成功')
|
||||
emit('activated', result.data)
|
||||
handleReset()
|
||||
} else {
|
||||
error.value = result.message || '激活失败'
|
||||
Message.error(result.message || '激活失败')
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message || '激活失败,请重试'
|
||||
Message.error('激活失败:' + (err.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
formData.value.licenseCode = ''
|
||||
error.value = ''
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setLoading: (val) => {
|
||||
loading.value = val
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
42
web/src/views/auth/AuthPage.vue
Normal file
42
web/src/views/auth/AuthPage.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="auth-page">
|
||||
<div class="container">
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="12">
|
||||
<ActivateForm @activated="handleActivated" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<AuthStatus ref="authStatusRef" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ActivateForm from './ActivateForm.vue'
|
||||
import AuthStatus from './AuthStatus.vue'
|
||||
|
||||
const authStatusRef = ref(null)
|
||||
|
||||
const handleActivated = () => {
|
||||
if (authStatusRef.value) {
|
||||
authStatusRef.value.refresh()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
104
web/src/views/auth/AuthStatus.vue
Normal file
104
web/src/views/auth/AuthStatus.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<template #title>授权状态</template>
|
||||
|
||||
<a-descriptions :column="1" bordered v-if="authStatus">
|
||||
<a-descriptions-item label="激活状态">
|
||||
<a-tag :color="authStatus.is_activated ? 'green' : 'red'">
|
||||
{{ authStatus.is_activated ? '已激活' : '未激活' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="状态信息">
|
||||
{{ authStatus.message }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="授权码" v-if="authStatus.license_code">
|
||||
{{ formatLicenseCode(authStatus.license_code) }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="激活时间" v-if="authStatus.activated_at">
|
||||
{{ formatDate(authStatus.activated_at) }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="过期时间" v-if="authStatus.expires_at">
|
||||
{{ formatDate(authStatus.expires_at) }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="设备ID">
|
||||
<a-typography-text copyable>{{ deviceID }}</a-typography-text>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-empty v-else description="加载中..." />
|
||||
|
||||
<template #actions>
|
||||
<a-button @click="handleRefresh" :loading="loading">刷新状态</a-button>
|
||||
</template>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { GetAuthStatus, GetDeviceID } from '../../wailsjs/go/main/App'
|
||||
|
||||
const authStatus = ref(null)
|
||||
const deviceID = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const formatLicenseCode = (code) => {
|
||||
if (!code) return ''
|
||||
const cleaned = code.replace(/[\s-]/g, '')
|
||||
return cleaned.match(/.{1,4}/g)?.join('-') || cleaned
|
||||
}
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
} catch {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
const loadAuthStatus = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [statusResult, deviceResult] = await Promise.all([
|
||||
GetAuthStatus(),
|
||||
GetDeviceID()
|
||||
])
|
||||
|
||||
if (statusResult.success) {
|
||||
authStatus.value = statusResult.data
|
||||
} else {
|
||||
Message.error(statusResult.message || '获取授权状态失败')
|
||||
}
|
||||
|
||||
if (deviceResult) {
|
||||
deviceID.value = deviceResult
|
||||
}
|
||||
} catch (err) {
|
||||
Message.error('获取授权状态失败:' + (err.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadAuthStatus()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAuthStatus()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
refresh: loadAuthStatus
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
134
web/src/views/data/BackupPanel.vue
Normal file
134
web/src/views/data/BackupPanel.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<template #title>数据备份与恢复</template>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<!-- 操作按钮 -->
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleBackup" :loading="backing">
|
||||
创建备份
|
||||
</a-button>
|
||||
<a-button @click="handleRefreshList" :loading="loading">
|
||||
刷新列表
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
||||
<!-- 备份列表 -->
|
||||
<a-table
|
||||
v-if="backups.length > 0"
|
||||
:columns="columns"
|
||||
:data="backups"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
>
|
||||
<template #operation="{ record }">
|
||||
<a-space>
|
||||
<a-button type="primary" size="small" @click="handleRestore(record)">
|
||||
恢复
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-else description="暂无备份文件" />
|
||||
|
||||
<!-- 恢复确认对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="restoreVisible"
|
||||
title="确认恢复"
|
||||
@ok="confirmRestore"
|
||||
@cancel="cancelRestore"
|
||||
>
|
||||
<p>确定要恢复此备份吗?恢复后将替换当前数据库,此操作不可逆!</p>
|
||||
<p><strong>备份文件:</strong>{{ selectedBackup?.file_name }}</p>
|
||||
<p><strong>创建时间:</strong>{{ selectedBackup?.created_at }}</p>
|
||||
</a-modal>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { BackupData, RestoreData, ListBackups } from '../../wailsjs/go/main/App'
|
||||
|
||||
const backups = ref([])
|
||||
const loading = ref(false)
|
||||
const backing = ref(false)
|
||||
const restoreVisible = ref(false)
|
||||
const selectedBackup = ref(null)
|
||||
|
||||
const columns = [
|
||||
{ title: '文件名', dataIndex: 'file_name', width: 250 },
|
||||
{ title: '文件大小', dataIndex: 'file_size', width: 120, slotName: 'size' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 180 },
|
||||
{ title: '操作', slotName: 'operation', width: 100 }
|
||||
]
|
||||
|
||||
// 加载备份列表
|
||||
const loadBackups = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await ListBackups()
|
||||
backups.value = result.backups || []
|
||||
} catch (error) {
|
||||
console.error('获取备份列表失败:', error)
|
||||
Message.error('获取备份列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建备份
|
||||
const handleBackup = async () => {
|
||||
backing.value = true
|
||||
try {
|
||||
const result = await BackupData()
|
||||
Message.success(`备份创建成功: ${result.file_name}`)
|
||||
await loadBackups()
|
||||
} catch (error) {
|
||||
console.error('备份失败:', error)
|
||||
Message.error('备份失败:' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
backing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复备份
|
||||
const handleRestore = (backup) => {
|
||||
selectedBackup.value = backup
|
||||
restoreVisible.value = true
|
||||
}
|
||||
|
||||
// 确认恢复
|
||||
const confirmRestore = async () => {
|
||||
if (!selectedBackup.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await RestoreData(selectedBackup.value.backup_path)
|
||||
Message.success('数据恢复成功')
|
||||
restoreVisible.value = false
|
||||
selectedBackup.value = null
|
||||
} catch (error) {
|
||||
console.error('恢复失败:', error)
|
||||
Message.error('恢复失败:' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 取消恢复
|
||||
const cancelRestore = () => {
|
||||
restoreVisible.value = false
|
||||
selectedBackup.value = null
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
const handleRefreshList = () => {
|
||||
loadBackups()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBackups()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
48
web/src/views/data/DataStats.vue
Normal file
48
web/src/views/data/DataStats.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<template #title>
|
||||
<span>数据统计</span>
|
||||
<a-button type="text" size="small" @click="handleRefresh" style="margin-left: 10px">
|
||||
刷新
|
||||
</a-button>
|
||||
</template>
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="数据总量">
|
||||
{{ stats?.total_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最新期号">
|
||||
{{ stats?.latest_issue || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { GetDataStats } from '../../wailsjs/go/main/App'
|
||||
|
||||
const stats = ref(null)
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const data = await GetDataStats()
|
||||
stats.value = data
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
Message.error('获取统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await loadStats()
|
||||
Message.success('统计数据已刷新')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
182
web/src/views/data/PackagePanel.vue
Normal file
182
web/src/views/data/PackagePanel.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<template #title>离线数据包管理</template>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<!-- 检查更新 -->
|
||||
<a-space>
|
||||
<a-input
|
||||
v-model="remoteURL"
|
||||
placeholder="输入数据包信息 URL"
|
||||
style="width: 400px"
|
||||
/>
|
||||
<a-button type="primary" @click="handleCheckUpdate" :loading="checking">
|
||||
检查更新
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
||||
<!-- 更新信息 -->
|
||||
<a-alert v-if="updateInfo" :type="updateInfo.need_update ? 'info' : 'success'">
|
||||
<template #title>{{ updateInfo.need_update ? '发现新数据包' : '已是最新版本' }}</template>
|
||||
<div v-if="updateInfo.need_update">
|
||||
<p>版本:{{ updateInfo.version }}</p>
|
||||
<p>数据总数:{{ updateInfo.total_count }}</p>
|
||||
<p>最新期号:{{ updateInfo.latest_issue }}</p>
|
||||
<p>文件大小:{{ formatFileSize(updateInfo.package_size) }}</p>
|
||||
<p>发布日期:{{ updateInfo.release_date }}</p>
|
||||
<a-button type="primary" @click="handleDownload" :loading="downloading" style="margin-top: 10px">
|
||||
下载数据包
|
||||
</a-button>
|
||||
</div>
|
||||
</a-alert>
|
||||
|
||||
<!-- 下载进度 -->
|
||||
<a-progress
|
||||
v-if="downloadProgress > 0 && downloadProgress < 100"
|
||||
:percent="downloadProgress"
|
||||
:status="downloadStatus"
|
||||
/>
|
||||
|
||||
<!-- 本地数据包列表 -->
|
||||
<a-divider />
|
||||
<a-space>
|
||||
<a-button @click="handleRefreshList" :loading="loading">刷新列表</a-button>
|
||||
</a-space>
|
||||
|
||||
<a-table
|
||||
v-if="packages.length > 0"
|
||||
:columns="columns"
|
||||
:data="packages"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
>
|
||||
<template #operation="{ record }">
|
||||
<a-space>
|
||||
<a-button type="primary" size="small" @click="handleImport(record)">
|
||||
导入
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-else description="暂无本地数据包" />
|
||||
</a-space>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { DownloadPackage, ImportPackage, CheckPackageUpdate, ListLocalPackages } from '../../wailsjs/go/main/App'
|
||||
|
||||
const remoteURL = ref('')
|
||||
const checking = ref(false)
|
||||
const downloading = ref(false)
|
||||
const loading = ref(false)
|
||||
const updateInfo = ref(null)
|
||||
const downloadProgress = ref(0)
|
||||
const downloadStatus = ref('normal')
|
||||
const packages = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '文件名', dataIndex: 'name', width: 300 },
|
||||
{ title: '文件路径', dataIndex: 'path', width: 400 },
|
||||
{ title: '操作', slotName: 'operation', width: 100 }
|
||||
]
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes) => {
|
||||
if (!bytes) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 检查更新
|
||||
const handleCheckUpdate = async () => {
|
||||
if (!remoteURL.value) {
|
||||
Message.warning('请输入数据包信息 URL')
|
||||
return
|
||||
}
|
||||
|
||||
checking.value = true
|
||||
try {
|
||||
const result = await CheckPackageUpdate(remoteURL.value)
|
||||
updateInfo.value = result
|
||||
if (result.need_update) {
|
||||
Message.success('发现新数据包')
|
||||
} else {
|
||||
Message.success('已是最新版本')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
Message.error('检查更新失败:' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下载数据包
|
||||
const handleDownload = async () => {
|
||||
if (!updateInfo.value?.download_url) {
|
||||
Message.warning('下载地址不存在')
|
||||
return
|
||||
}
|
||||
|
||||
downloading.value = true
|
||||
downloadProgress.value = 0
|
||||
downloadStatus.value = 'active'
|
||||
|
||||
try {
|
||||
const result = await DownloadPackage(updateInfo.value.download_url)
|
||||
downloadProgress.value = 100
|
||||
downloadStatus.value = 'success'
|
||||
Message.success('数据包下载成功')
|
||||
await loadPackages()
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
downloadStatus.value = 'exception'
|
||||
Message.error('下载失败:' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
downloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导入数据包
|
||||
const handleImport = async (pkg) => {
|
||||
try {
|
||||
const result = await ImportPackage(pkg.path)
|
||||
Message.success(`导入成功:导入 ${result.imported_count} 条数据`)
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error)
|
||||
Message.error('导入失败:' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 加载本地数据包列表
|
||||
const loadPackages = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await ListLocalPackages()
|
||||
packages.value = (result.packages || []).map(path => ({
|
||||
name: path.split(/[/\\]/).pop(),
|
||||
path: path
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取数据包列表失败:', error)
|
||||
Message.error('获取数据包列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
const handleRefreshList = () => {
|
||||
loadPackages()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPackages()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
107
web/src/views/data/SyncPanel.vue
Normal file
107
web/src/views/data/SyncPanel.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<template #title>数据同步</template>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<!-- 同步状态 -->
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="本地数据量">
|
||||
{{ syncStatus?.local_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="本地最新期号">
|
||||
{{ syncStatus?.local_latest_issue || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="远程数据量">
|
||||
{{ syncStatus?.remote_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="远程最新期号">
|
||||
{{ syncStatus?.remote_latest_issue || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="同步状态" :span="2">
|
||||
<a-tag :color="syncStatus?.need_sync ? 'orange' : 'green'">
|
||||
{{ syncStatus?.need_sync ? '需要同步' : '已是最新' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSync" :loading="syncing">
|
||||
开始同步
|
||||
</a-button>
|
||||
<a-button @click="handleRefreshStatus" :loading="refreshing">
|
||||
刷新状态
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
||||
<!-- 同步结果 -->
|
||||
<a-alert v-if="syncResult" :type="syncResult.error_count > 0 ? 'warning' : 'success'">
|
||||
<template #title>同步完成</template>
|
||||
<div>总数据量:{{ syncResult.total_count }}</div>
|
||||
<div>同步数量:{{ syncResult.synced_count }}</div>
|
||||
<div>新增数量:{{ syncResult.new_count }}</div>
|
||||
<div>更新数量:{{ syncResult.updated_count }}</div>
|
||||
<div v-if="syncResult.error_count > 0">错误数量:{{ syncResult.error_count }}</div>
|
||||
<div v-if="syncResult.latest_issue">最新期号:{{ syncResult.latest_issue }}</div>
|
||||
</a-alert>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { SyncData, GetSyncStatus } from '../../wailsjs/go/main/App'
|
||||
|
||||
const syncStatus = ref(null)
|
||||
const syncing = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const syncResult = ref(null)
|
||||
|
||||
// 加载同步状态
|
||||
const loadSyncStatus = async () => {
|
||||
try {
|
||||
const status = await GetSyncStatus()
|
||||
syncStatus.value = status
|
||||
} catch (error) {
|
||||
console.error('获取同步状态失败:', error)
|
||||
Message.error('获取同步状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 执行同步
|
||||
const handleSync = async () => {
|
||||
syncing.value = true
|
||||
syncResult.value = null
|
||||
|
||||
try {
|
||||
const result = await SyncData()
|
||||
syncResult.value = result
|
||||
Message.success('同步完成')
|
||||
// 同步后刷新状态
|
||||
await loadSyncStatus()
|
||||
} catch (error) {
|
||||
console.error('同步失败:', error)
|
||||
Message.error('同步失败:' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态
|
||||
const handleRefreshStatus = async () => {
|
||||
refreshing.value = true
|
||||
try {
|
||||
await loadSyncStatus()
|
||||
Message.success('状态已刷新')
|
||||
} finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSyncStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
179
web/src/views/query/QueryForm.vue
Normal file
179
web/src/views/query/QueryForm.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<template #title>查询条件</template>
|
||||
<a-form :model="formData" layout="vertical">
|
||||
<!-- 红球输入 -->
|
||||
<a-form-item label="红球号码" class="inline-form-item">
|
||||
<div class="red-balls">
|
||||
<a-input-number
|
||||
v-for="(ball, index) in redBalls"
|
||||
:key="index"
|
||||
v-model="redBalls[index]"
|
||||
:min="1"
|
||||
:max="33"
|
||||
:precision="0"
|
||||
placeholder="红球"
|
||||
class="red-ball-input"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 蓝球输入 -->
|
||||
<a-form-item label="蓝球号码" class="inline-form-item">
|
||||
<a-input-number
|
||||
v-model="formData.blueBall"
|
||||
:min="1"
|
||||
:max="16"
|
||||
:precision="0"
|
||||
placeholder="蓝球(不填表示不限制)"
|
||||
style="width: 200px"
|
||||
:allow-clear="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 蓝球筛选 -->
|
||||
<a-form-item label="蓝球筛选范围">
|
||||
<div class="blue-ball-filter">
|
||||
<div class="blue-ball-item">
|
||||
<a-checkbox v-model="selectAll" @change="handleSelectAll" />
|
||||
<span class="blue-ball-label">全选</span>
|
||||
</div>
|
||||
<a-checkbox-group v-model="formData.blueBallRange" class="blue-checkboxes">
|
||||
<div v-for="i in 16" :key="i" class="blue-ball-item">
|
||||
<a-checkbox :value="i" />
|
||||
<span class="blue-ball-label">蓝球{{ i }}</span>
|
||||
</div>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleQuery" :loading="loading">查询</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
|
||||
const emit = defineEmits(['query', 'reset'])
|
||||
|
||||
const formData = ref({
|
||||
blueBall: undefined,
|
||||
blueBallRange: []
|
||||
})
|
||||
|
||||
const redBalls = ref([null, null, null, null, null, null])
|
||||
const loading = ref(false)
|
||||
const selectAll = ref(true)
|
||||
|
||||
// 全选逻辑
|
||||
const handleSelectAll = (checked) => {
|
||||
if (checked) {
|
||||
formData.value.blueBallRange = Array.from({ length: 16 }, (_, i) => i + 1)
|
||||
} else {
|
||||
formData.value.blueBallRange = []
|
||||
}
|
||||
}
|
||||
|
||||
// 监听蓝球筛选变化
|
||||
watch(() => formData.value.blueBallRange, (newVal) => {
|
||||
selectAll.value = newVal.length === 16
|
||||
}, { deep: true })
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
const redBallsFiltered = redBalls.value.filter(ball => ball !== null && ball > 0)
|
||||
emit('query', {
|
||||
redBalls: redBallsFiltered,
|
||||
blueBall: formData.value.blueBall || 0,
|
||||
blueBallRange: formData.value.blueBallRange.length > 0 ? formData.value.blueBallRange : []
|
||||
})
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
redBalls.value = [null, null, null, null, null, null]
|
||||
formData.value.blueBall = undefined
|
||||
formData.value.blueBallRange = Array.from({ length: 16 }, (_, i) => i + 1)
|
||||
selectAll.value = true
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setLoading: (val) => {
|
||||
loading.value = val
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.inline-form-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.inline-form-item .arco-form-item-label) {
|
||||
margin-bottom: 0 !important;
|
||||
margin-right: 12px;
|
||||
padding-right: 12px;
|
||||
flex-shrink: 0;
|
||||
width: auto;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
:deep(.inline-form-item .arco-form-item-content) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.red-balls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.red-ball-input {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.blue-ball-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.blue-ball-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.blue-ball-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.blue-checkboxes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
102
web/src/views/query/QueryPage.vue
Normal file
102
web/src/views/query/QueryPage.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="query-page">
|
||||
<div class="container">
|
||||
<!-- 查询条件 -->
|
||||
<QueryForm
|
||||
ref="queryFormRef"
|
||||
@query="handleQuery"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
<!-- 查询结果 -->
|
||||
<ResultPanel
|
||||
v-if="queryResult"
|
||||
:summary="queryResult.summary || []"
|
||||
:details="queryResult.details || []"
|
||||
:query-red-balls="currentQuery.redBalls"
|
||||
:query-blue-ball="currentQuery.blueBall"
|
||||
:query-blue-ball-range="currentQuery.blueBallRange"
|
||||
@summary-click="handleSummaryClick"
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<a-spin v-if="loading" :style="{ width: '100%', marginTop: '12px' }" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import QueryForm from './QueryForm.vue'
|
||||
import ResultPanel from './ResultPanel.vue'
|
||||
import { QueryHistory } from '../../wailsjs/go/main/App'
|
||||
|
||||
const queryFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const queryResult = ref(null)
|
||||
const currentQuery = ref({
|
||||
redBalls: [],
|
||||
blueBall: 0,
|
||||
blueBallRange: []
|
||||
})
|
||||
|
||||
// 查询
|
||||
const handleQuery = async (queryParams) => {
|
||||
if (!queryParams.redBalls || queryParams.redBalls.length === 0) {
|
||||
Message.warning('请至少输入一个红球号码')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
currentQuery.value = queryParams
|
||||
|
||||
try {
|
||||
const result = await QueryHistory({
|
||||
red_balls: queryParams.redBalls,
|
||||
blue_ball: queryParams.blueBall,
|
||||
blue_ball_range: queryParams.blueBallRange
|
||||
})
|
||||
|
||||
if (result) {
|
||||
queryResult.value = result
|
||||
Message.success('查询成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error('查询失败:' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryResult.value = null
|
||||
currentQuery.value = {
|
||||
redBalls: [],
|
||||
blueBall: 0,
|
||||
blueBallRange: []
|
||||
}
|
||||
}
|
||||
|
||||
// 点击汇总项
|
||||
const handleSummaryClick = (item) => {
|
||||
console.log('点击汇总项:', item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.query-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
198
web/src/views/query/ResultPanel.vue
Normal file
198
web/src/views/query/ResultPanel.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<a-layout class="result-layout">
|
||||
<!-- 左侧汇总列表 -->
|
||||
<a-layout-sider width="280" class="summary-sider">
|
||||
<a-card>
|
||||
<template #title>查询汇总</template>
|
||||
<a-list v-if="summary.length > 0" :data="summary">
|
||||
<template #item="{ item }">
|
||||
<a-list-item class="summary-item" @click="handleSummaryClick(item)">
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<span>{{ item.type }}:{{ item.count }}次</span>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-button type="text" size="small">显示历史开奖</a-button>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-empty v-else description="暂无数据" />
|
||||
</a-card>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 右侧详情列表 -->
|
||||
<a-layout-content class="detail-content">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<span>查询结果详情</span>
|
||||
<a-button v-if="selectedSummary" type="text" size="small" @click="clearSelection" style="margin-left: 8px">
|
||||
清除筛选
|
||||
</a-button>
|
||||
</template>
|
||||
<a-table
|
||||
v-if="displayDetails.length > 0"
|
||||
:columns="columns"
|
||||
:data="displayDetails"
|
||||
:pagination="paginationConfig"
|
||||
size="mini"
|
||||
row-key="id"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
>
|
||||
<template #ball="{ record, column }">
|
||||
<span
|
||||
:style="getBallStyle(record, column.dataIndex)"
|
||||
>
|
||||
{{ record[column.dataIndex] }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-else description="暂无数据" />
|
||||
</a-card>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
summary: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
details: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
queryRedBalls: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
queryBlueBall: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
queryBlueBallRange: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['summary-click'])
|
||||
|
||||
const selectedSummary = ref(null)
|
||||
const pageSize = ref(20)
|
||||
|
||||
// 分页配置
|
||||
const paginationConfig = computed(() => ({
|
||||
pageSize: pageSize.value,
|
||||
pageSizeOptions: [20, 30, 50, 100, 200],
|
||||
showPageSize: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
}))
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: '期号', dataIndex: 'issue_number', align: 'center' },
|
||||
{ title: '红球1', dataIndex: 'red_ball_1', slotName: 'ball', align: 'center' },
|
||||
{ title: '红球2', dataIndex: 'red_ball_2', slotName: 'ball', align: 'center' },
|
||||
{ title: '红球3', dataIndex: 'red_ball_3', slotName: 'ball', align: 'center' },
|
||||
{ title: '红球4', dataIndex: 'red_ball_4', slotName: 'ball', align: 'center' },
|
||||
{ title: '红球5', dataIndex: 'red_ball_5', slotName: 'ball', align: 'center' },
|
||||
{ title: '红球6', dataIndex: 'red_ball_6', slotName: 'ball', align: 'center' },
|
||||
{ title: '蓝球', dataIndex: 'blue_ball', slotName: 'ball', align: 'center' }
|
||||
]
|
||||
|
||||
// 显示的数据
|
||||
const displayDetails = computed(() => {
|
||||
if (selectedSummary.value) {
|
||||
return selectedSummary.value.histories || []
|
||||
}
|
||||
return props.details
|
||||
})
|
||||
|
||||
// 点击汇总项
|
||||
const handleSummaryClick = (item) => {
|
||||
selectedSummary.value = item
|
||||
emit('summary-click', item)
|
||||
}
|
||||
|
||||
// 清除筛选
|
||||
const clearSelection = () => {
|
||||
selectedSummary.value = null
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page) => {
|
||||
// 页面变化处理(如果需要)
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handlePageSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
}
|
||||
|
||||
// 获取球的样式
|
||||
const getBallStyle = (record, field) => {
|
||||
if (field.startsWith('red_ball_')) {
|
||||
const ballValue = record[field]
|
||||
// 检查是否在查询的红球列表中
|
||||
if (props.queryRedBalls.includes(ballValue)) {
|
||||
return { color: '#F53F3F', fontWeight: 'bold' }
|
||||
}
|
||||
} else if (field === 'blue_ball') {
|
||||
const ballValue = record[field]
|
||||
// 检查是否匹配查询的蓝球(精确匹配或在筛选范围内)
|
||||
if (props.queryBlueBall > 0 && ballValue === props.queryBlueBall) {
|
||||
return { color: '#165DFF', fontWeight: 'bold' }
|
||||
}
|
||||
// 检查是否在蓝球筛选范围内
|
||||
if (props.queryBlueBallRange.length > 0 && props.queryBlueBallRange.includes(ballValue)) {
|
||||
return { color: '#165DFF', fontWeight: 'bold' }
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.summary-sider {
|
||||
background: var(--color-bg-1);
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.summary-item:hover {
|
||||
background: var(--color-fill-2);
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 0;
|
||||
background: var(--color-bg-1);
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
:deep(.arco-table) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-table table) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user