111
This commit is contained in:
@@ -9,6 +9,7 @@ import UsersPage from './components/pages/UsersPage'
|
||||
import EquipmentPage from './components/pages/EquipmentPage'
|
||||
import StatisticsPage from './components/pages/StatisticsPage'
|
||||
import MallsPage from './components/pages/MallsPage'
|
||||
import VersionPage from './components/pages/VersionPage'
|
||||
import DynamicPage from './components/DynamicPage'
|
||||
|
||||
function App() {
|
||||
@@ -25,6 +26,7 @@ function App() {
|
||||
<Route path="users" element={<UsersPage />} />
|
||||
<Route path="equipment" element={<EquipmentPage />} />
|
||||
<Route path="malls" element={<MallsPage />} />
|
||||
<Route path="version" element={<VersionPage />} />
|
||||
<Route path=":slug" element={<DynamicPage />} />
|
||||
<Route path=":slug/*" element={<DynamicPage />} />
|
||||
</Route>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog"
|
||||
import { Plus, Search, Filter, Download, Calendar, AlertTriangle, Store, Eye, Building2, MapPin } from "lucide-react"
|
||||
import { Plus, Search, Filter, Download, Calendar, AlertTriangle, Store, Eye, Building2, MapPin, ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { apiGet, apiPost, apiPut } from "../../lib/services/api"
|
||||
|
||||
// 总商户数据类型
|
||||
@@ -77,6 +77,8 @@ export default function MerchantsPage() {
|
||||
const [merchantEquipments, setMerchantEquipments] = useState<{[key: string]: any[]}>({})
|
||||
const [loadingMerchantEquipments, setLoadingMerchantEquipments] = useState<{[key: string]: boolean}>({})
|
||||
const [userRole, setUserRole] = useState<string>("")
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
|
||||
const [newMerchant, setNewMerchant] = useState({
|
||||
name: "",
|
||||
@@ -213,10 +215,10 @@ export default function MerchantsPage() {
|
||||
}
|
||||
|
||||
// 获取商户列表
|
||||
const fetchMerchants = async () => {
|
||||
const fetchMerchants = async (page = currentPage, size = pageSize) => {
|
||||
setLoadingMerchants(true)
|
||||
try {
|
||||
const response = await apiGet('/back/merchants/list')
|
||||
const response = await apiGet(`/back/merchants/list?pageNum=${page}&pageSize=${size}`)
|
||||
if (response.code === 200) {
|
||||
const merchantsData = response.rows || []
|
||||
setTotal(parseInt(response.total) || 0)
|
||||
@@ -245,6 +247,19 @@ export default function MerchantsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 页码变化处理
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
fetchMerchants(page, pageSize)
|
||||
}
|
||||
|
||||
// 每页大小变化处理
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
setPageSize(size)
|
||||
setCurrentPage(1)
|
||||
fetchMerchants(1, size)
|
||||
}
|
||||
|
||||
// 组件加载时获取数据
|
||||
useEffect(() => {
|
||||
fetchUserRole()
|
||||
@@ -323,7 +338,7 @@ export default function MerchantsPage() {
|
||||
setIsAddMerchantOpen(false)
|
||||
|
||||
// 刷新商户列表
|
||||
fetchMerchants()
|
||||
fetchMerchants(currentPage, pageSize)
|
||||
} else {
|
||||
console.error('添加商户失败:', result)
|
||||
alert('添加商户失败:' + (result.msg || '未知错误'))
|
||||
@@ -460,7 +475,7 @@ export default function MerchantsPage() {
|
||||
console.log('编辑商户成功:', result)
|
||||
alert('编辑商户成功!')
|
||||
setIsEditMerchantOpen(false)
|
||||
fetchMerchants()
|
||||
fetchMerchants(currentPage, pageSize)
|
||||
} else {
|
||||
console.error('编辑商户失败:', result)
|
||||
alert('编辑商户失败:' + (result.msg || '未知错误'))
|
||||
@@ -576,7 +591,7 @@ export default function MerchantsPage() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>商户列表</CardTitle>
|
||||
<CardDescription>查看和管理所有商户信息及设备状态</CardDescription>
|
||||
<CardDescription>查看和管理所有商户信息及设备状态 - 共 {total} 条记录,当前第 {currentPage} 页</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
@@ -705,6 +720,56 @@ export default function MerchantsPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between space-x-2 py-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">每页显示</p>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={(value) => handlePageSizeChange(parseInt(value))}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="20">20</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
共 {total} 条记录
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">
|
||||
第 {currentPage} 页,共 {Math.ceil(total / pageSize)} 页
|
||||
</p>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1 || loadingMerchants}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span className="sr-only">上一页</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= Math.ceil(total / pageSize) || loadingMerchants}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<span className="sr-only">下一页</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
803
src/components/pages/VersionPage.tsx
Normal file
803
src/components/pages/VersionPage.tsx
Normal file
@@ -0,0 +1,803 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
|
||||
import { Button } from "../ui/button"
|
||||
import { Input } from "../ui/input"
|
||||
import { Badge } from "../ui/badge"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog"
|
||||
import { Label } from "../ui/label"
|
||||
import { Textarea } from "../ui/textarea"
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Download,
|
||||
Edit,
|
||||
Trash2,
|
||||
Upload,
|
||||
RefreshCw,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from "lucide-react"
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from "../../lib/services/api"
|
||||
import { getUserToken } from "../../lib/utils/storage"
|
||||
|
||||
// 版本信息数据类型接口
|
||||
interface Version {
|
||||
id: string
|
||||
versionCode: number
|
||||
versionName: string
|
||||
isForce: string
|
||||
channel: string
|
||||
downloadUrl: string
|
||||
updateLog: string
|
||||
size: string
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
// 渠道中文名称映射
|
||||
const channelNameMap: Record<string, string> = {
|
||||
'official': '官方',
|
||||
'xiaomi': '小米',
|
||||
'huawei': '华为',
|
||||
'oppo': 'OPPO',
|
||||
'vivo': 'VIVO',
|
||||
'meizu': '魅族',
|
||||
'honor': '荣耀',
|
||||
'yingyongbao': '应用宝',
|
||||
'ios': 'iOS'
|
||||
}
|
||||
|
||||
export default function VersionPage() {
|
||||
const [versions, setVersions] = useState<Version[]>([])
|
||||
const [channels, setChannels] = useState<string[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [isForceFilter, setIsForceFilter] = useState("all")
|
||||
const [channelFilter, setChannelFilter] = useState("all")
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
versionName: "",
|
||||
versionCode: 4,
|
||||
isForce: "0",
|
||||
channel: "",
|
||||
downloadUrl: "",
|
||||
updateLog: "",
|
||||
size: "",
|
||||
})
|
||||
|
||||
const [editData, setEditData] = useState<Version | null>(null)
|
||||
|
||||
// 获取版本列表
|
||||
const fetchVersions = async (page = currentPage, size = pageSize) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params: any = {
|
||||
pageNum: page,
|
||||
pageSize: size,
|
||||
}
|
||||
|
||||
if (searchTerm) params.versionName = searchTerm
|
||||
if (isForceFilter !== "all") params.isForce = isForceFilter
|
||||
if (channelFilter !== "all") params.channel = channelFilter
|
||||
|
||||
const response = await apiGet('/back/version/list', params)
|
||||
if (response.code === 200) {
|
||||
setVersions(response.rows || [])
|
||||
setTotal(parseInt(response.total) || 0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取版本列表失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取渠道列表
|
||||
const fetchChannels = async () => {
|
||||
try {
|
||||
const response = await apiGet('/back/version/getChannel')
|
||||
if (response.code === 200) {
|
||||
setChannels(response.data || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取渠道列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件加载时获取数据
|
||||
useEffect(() => {
|
||||
fetchVersions()
|
||||
fetchChannels()
|
||||
}, [])
|
||||
|
||||
// 分页变化时重新获取数据
|
||||
useEffect(() => {
|
||||
fetchVersions(currentPage, pageSize)
|
||||
}, [currentPage, pageSize])
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) {
|
||||
console.log('没有选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('选择的文件:', file.name, '大小:', file.size)
|
||||
|
||||
// 检查文件大小(500MB限制)
|
||||
const maxSize = 500 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
alert('上传文件大小不能超过 500MB!')
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedFile(file)
|
||||
|
||||
// 计算文件大小
|
||||
const fileSize = file.size
|
||||
let sizeText = ''
|
||||
if (fileSize < 1024) {
|
||||
sizeText = fileSize + ' B'
|
||||
} else if (fileSize < 1024 * 1024) {
|
||||
sizeText = (fileSize / 1024).toFixed(2) + ' KB'
|
||||
} else if (fileSize < 1024 * 1024 * 1024) {
|
||||
sizeText = (fileSize / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
} else {
|
||||
sizeText = (fileSize / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
|
||||
}
|
||||
|
||||
console.log('文件大小:', sizeText)
|
||||
|
||||
if (isEditDialogOpen && editData) {
|
||||
setEditData({ ...editData, size: sizeText })
|
||||
} else {
|
||||
setFormData({ ...formData, size: sizeText })
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
console.log('开始上传文件...')
|
||||
await uploadFile(file)
|
||||
}
|
||||
|
||||
// 上传文件到服务器
|
||||
const uploadFile = async (file: File) => {
|
||||
console.log('uploadFile 函数被调用')
|
||||
const formDataUpload = new FormData()
|
||||
formDataUpload.append('file', file)
|
||||
|
||||
console.log('准备上传的文件:', file.name)
|
||||
|
||||
try {
|
||||
const token = getUserToken()
|
||||
console.log('Token:', token ? '存在' : '不存在')
|
||||
|
||||
// 使用当前页面的域名和端口,自动适配开发和生产环境
|
||||
const baseUrl = window.location.origin.includes('localhost')
|
||||
? 'http://localhost:8080/api'
|
||||
: '/api'
|
||||
const url = `${baseUrl}/back/version/upload`
|
||||
console.log('上传URL:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formDataUpload
|
||||
})
|
||||
|
||||
console.log('响应状态:', response.status)
|
||||
const result = await response.json()
|
||||
console.log('响应结果:', result)
|
||||
|
||||
if (result.code === 200) {
|
||||
if (isEditDialogOpen && editData) {
|
||||
setEditData({ ...editData, downloadUrl: result.data })
|
||||
} else {
|
||||
setFormData({ ...formData, downloadUrl: result.data })
|
||||
}
|
||||
alert('文件上传成功')
|
||||
} else {
|
||||
alert('文件上传失败:' + result.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error)
|
||||
alert('文件上传失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 添加版本
|
||||
const handleAddVersion = async () => {
|
||||
if (!formData.versionName || !formData.versionCode || !formData.channel || !formData.downloadUrl) {
|
||||
alert('请填写所有必填字段')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const response = await apiPost('/back/version', {
|
||||
...formData,
|
||||
versionCode: Number(formData.versionCode)
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
alert('新增成功')
|
||||
setIsAddDialogOpen(false)
|
||||
resetForm()
|
||||
fetchVersions(currentPage, pageSize)
|
||||
} else {
|
||||
alert('新增失败:' + response.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('新增版本失败:', error)
|
||||
alert('新增失败,请稍后重试')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑对话框
|
||||
const handleEditVersion = async (version: Version) => {
|
||||
setEditData(version)
|
||||
setIsEditDialogOpen(true)
|
||||
}
|
||||
|
||||
// 更新版本
|
||||
const handleUpdateVersion = async () => {
|
||||
if (!editData) return
|
||||
|
||||
if (!editData.versionName || !editData.versionCode || !editData.channel || !editData.downloadUrl) {
|
||||
alert('请填写所有必填字段')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const response = await apiPut('/back/version', {
|
||||
...editData,
|
||||
versionCode: Number(editData.versionCode)
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
alert('修改成功')
|
||||
setIsEditDialogOpen(false)
|
||||
setEditData(null)
|
||||
fetchVersions(currentPage, pageSize)
|
||||
} else {
|
||||
alert('修改失败:' + response.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改版本失败:', error)
|
||||
alert('修改失败,请稍后重试')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除版本
|
||||
const handleDeleteVersion = async (id: string) => {
|
||||
if (!window.confirm('是否确认删除该版本信息?')) return
|
||||
|
||||
try {
|
||||
const response = await apiDelete(`/back/version/${id}`)
|
||||
if (response.code === 200) {
|
||||
alert('删除成功')
|
||||
fetchVersions(currentPage, pageSize)
|
||||
} else {
|
||||
alert('删除失败:' + response.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除版本失败:', error)
|
||||
alert('删除失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
versionName: "",
|
||||
versionCode: 4,
|
||||
isForce: "0",
|
||||
channel: "",
|
||||
downloadUrl: "",
|
||||
updateLog: "",
|
||||
size: "",
|
||||
})
|
||||
setSelectedFile(null)
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1)
|
||||
fetchVersions(1, pageSize)
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
setSearchTerm("")
|
||||
setIsForceFilter("all")
|
||||
setChannelFilter("all")
|
||||
setCurrentPage(1)
|
||||
fetchVersions(1, pageSize)
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = () => {
|
||||
// 实现导出功能
|
||||
alert('导出功能开发中')
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
setPageSize(size)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">版本管理</h1>
|
||||
<p className="text-gray-600">管理应用版本更新信息</p>
|
||||
</div>
|
||||
|
||||
{/* 搜索表单 */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||
<div>
|
||||
<Label>版本名称</Label>
|
||||
<Input
|
||||
placeholder="请输入版本名称"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>是否强制更新</Label>
|
||||
<Select value={isForceFilter} onValueChange={setIsForceFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="0">否</SelectItem>
|
||||
<SelectItem value="1">是</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>渠道标识</Label>
|
||||
<Select value={channelFilter} onValueChange={setChannelFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部渠道</SelectItem>
|
||||
{channels.map((channel) => (
|
||||
<SelectItem key={channel} value={channel.toLowerCase()}>
|
||||
{channelNameMap[channel.toLowerCase()] || channel.toLowerCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<Button onClick={handleSearch}>
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
搜索
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => setIsAddDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新增
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleExport}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 版本列表 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>版本列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>主键ID</TableHead>
|
||||
<TableHead>版本代码</TableHead>
|
||||
<TableHead>版本名称</TableHead>
|
||||
<TableHead>强制更新</TableHead>
|
||||
<TableHead>渠道标识</TableHead>
|
||||
<TableHead>APK下载</TableHead>
|
||||
<TableHead>更新日志</TableHead>
|
||||
<TableHead>安装包</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-8">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : versions.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-8">
|
||||
<div className="text-gray-500">暂无数据</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
versions.map((version) => (
|
||||
<TableRow key={version.id}>
|
||||
<TableCell>{version.id}</TableCell>
|
||||
<TableCell>{version.versionCode}</TableCell>
|
||||
<TableCell>{version.versionName}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={version.isForce === '1' ? "bg-red-100 text-red-800" : "bg-green-100 text-green-800"}>
|
||||
{version.isForce === '1' ? '是' : '否'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">
|
||||
{channelNameMap[version.channel] || version.channel}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-xs truncate" title={version.downloadUrl}>
|
||||
{version.downloadUrl}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-xs truncate" title={version.updateLog}>
|
||||
{version.updateLog || '-'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{version.size}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditVersion(version)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteVersion(version.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
<div className="flex items-center justify-between space-x-2 py-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">每页显示</p>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={(value) => handlePageSizeChange(parseInt(value))}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="20">20</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
共 {total} 条记录
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">
|
||||
第 {currentPage} 页,共 {Math.ceil(total / pageSize)} 页
|
||||
</p>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1 || loading}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= Math.ceil(total / pageSize) || loading}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 新增对话框 */}
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加应用版本更新信息</DialogTitle>
|
||||
<DialogDescription>填写版本信息并上传APK文件</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="versionName">版本名称 <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
id="versionName"
|
||||
value={formData.versionName}
|
||||
onChange={(e) => setFormData({ ...formData, versionName: e.target.value })}
|
||||
placeholder="请输入版本名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="versionCode">版本代码 <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
id="versionCode"
|
||||
type="number"
|
||||
min="4"
|
||||
value={formData.versionCode}
|
||||
onChange={(e) => setFormData({ ...formData, versionCode: parseInt(e.target.value) || 4 })}
|
||||
placeholder="请输入版本代码"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="isForce">强制更新 <span className="text-red-500">*</span></Label>
|
||||
<Select value={formData.isForce} onValueChange={(value) => setFormData({ ...formData, isForce: value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">否</SelectItem>
|
||||
<SelectItem value="1">是</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="channel">渠道标识 <span className="text-red-500">*</span></Label>
|
||||
<Select value={formData.channel} onValueChange={(value) => setFormData({ ...formData, channel: value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择渠道" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{channels.map((channel) => (
|
||||
<SelectItem key={channel} value={channel.toLowerCase()}>
|
||||
{channelNameMap[channel.toLowerCase()] || channel.toLowerCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="file">APK下载 <span className="text-red-500">*</span></Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="file"
|
||||
type="file"
|
||||
accept=".apk"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => document.getElementById('file')?.click()}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
点击上传
|
||||
</Button>
|
||||
<span className="text-sm text-gray-500">文件大小不超过500MB</span>
|
||||
</div>
|
||||
<Input
|
||||
value={formData.downloadUrl}
|
||||
placeholder="文件上传后自动填入地址"
|
||||
readOnly
|
||||
className="mt-2 bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="updateLog">更新日志</Label>
|
||||
<Textarea
|
||||
id="updateLog"
|
||||
value={formData.updateLog}
|
||||
onChange={(e) => setFormData({ ...formData, updateLog: e.target.value })}
|
||||
placeholder="请输入更新日志"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="size">安装包大小</Label>
|
||||
<Input
|
||||
id="size"
|
||||
value={formData.size}
|
||||
onChange={(e) => setFormData({ ...formData, size: e.target.value })}
|
||||
placeholder="上传文件后自动填入"
|
||||
readOnly
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => { setIsAddDialogOpen(false); resetForm(); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleAddVersion} disabled={submitting}>
|
||||
{submitting ? "提交中..." : "确定"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 编辑对话框 */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>修改应用版本更新信息</DialogTitle>
|
||||
<DialogDescription>修改版本信息</DialogDescription>
|
||||
</DialogHeader>
|
||||
{editData && (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-versionName">版本名称 <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
id="edit-versionName"
|
||||
value={editData.versionName}
|
||||
onChange={(e) => setEditData({ ...editData, versionName: e.target.value })}
|
||||
placeholder="请输入版本名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-versionCode">版本代码 <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
id="edit-versionCode"
|
||||
type="number"
|
||||
min="4"
|
||||
value={editData.versionCode}
|
||||
onChange={(e) => setEditData({ ...editData, versionCode: parseInt(e.target.value) || 4 })}
|
||||
placeholder="请输入版本代码"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-isForce">强制更新 <span className="text-red-500">*</span></Label>
|
||||
<Select value={editData.isForce} onValueChange={(value) => setEditData({ ...editData, isForce: value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">否</SelectItem>
|
||||
<SelectItem value="1">是</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-channel">渠道标识 <span className="text-red-500">*</span></Label>
|
||||
<Select value={editData.channel} onValueChange={(value) => setEditData({ ...editData, channel: value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择渠道" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{channels.map((channel) => (
|
||||
<SelectItem key={channel} value={channel.toLowerCase()}>
|
||||
{channelNameMap[channel.toLowerCase()] || channel.toLowerCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-file">APK下载 <span className="text-red-500">*</span></Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="edit-file"
|
||||
type="file"
|
||||
accept=".apk"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => document.getElementById('edit-file')?.click()}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
点击上传
|
||||
</Button>
|
||||
<span className="text-sm text-gray-500">文件大小不超过500MB</span>
|
||||
</div>
|
||||
<Input
|
||||
value={editData.downloadUrl}
|
||||
placeholder="文件上传后自动填入地址"
|
||||
readOnly
|
||||
className="mt-2 bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-updateLog">更新日志</Label>
|
||||
<Textarea
|
||||
id="edit-updateLog"
|
||||
value={editData.updateLog}
|
||||
onChange={(e) => setEditData({ ...editData, updateLog: e.target.value })}
|
||||
placeholder="请输入更新日志"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-size">安装包大小</Label>
|
||||
<Input
|
||||
id="edit-size"
|
||||
value={editData.size}
|
||||
onChange={(e) => setEditData({ ...editData, size: e.target.value })}
|
||||
placeholder="上传文件后自动填入"
|
||||
readOnly
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => { setIsEditDialogOpen(false); setEditData(null); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleUpdateVersion} disabled={submitting}>
|
||||
{submitting ? "提交中..." : "确定"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -594,12 +594,15 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
|
||||
}
|
||||
|
||||
// 获取商户设备列表
|
||||
const fetchMerchantEquipments = async (merchantsId: string) => {
|
||||
const fetchMerchantEquipments = async (merchantId: string) => {
|
||||
setLoadingEquipment(true)
|
||||
console.log('获取商户设备,商户ID:', merchantId)
|
||||
try {
|
||||
const response = await apiGet(`/back/equipment/list?pageNum=1&pageSize=9999&merchantsId=${merchantsId}`)
|
||||
const response = await apiGet(`/back/equipment/list?pageNum=1&pageSize=9999&merchantId=${merchantId}`)
|
||||
console.log('设备列表响应:', response)
|
||||
if (response.code === 200) {
|
||||
setAvailableEquipment(response.rows || [])
|
||||
console.log('设备列表:', response.rows)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
@@ -626,16 +629,17 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
|
||||
}
|
||||
|
||||
const handleMerchantChange = (merchantId: string) => {
|
||||
console.log('选择商户,ID:', merchantId)
|
||||
setSelectedMerchant(merchantId)
|
||||
setFormData({ ...formData, merchantId, equipmentId: "" })
|
||||
|
||||
// 找到选中的商户对象,获取merchantsId
|
||||
// 找到选中的商户对象,使用 merchantsId 来获取设备列表
|
||||
const selectedMerchantObj = merchants.find(m => m.id === merchantId)
|
||||
if (selectedMerchantObj && selectedMerchantObj.merchantsId) {
|
||||
fetchMerchantEquipments(selectedMerchantObj.merchantsId)
|
||||
} else {
|
||||
// 如果没有merchantsId,使用id作为fallback
|
||||
fetchMerchantEquipments(merchantId)
|
||||
if (selectedMerchantObj) {
|
||||
// 使用 merchantsId 来查询设备,如果没有则使用 id
|
||||
const merchantIdForEquipment = selectedMerchantObj.merchantsId || selectedMerchantObj.id
|
||||
console.log('用于查询设备的 merchantId:', merchantIdForEquipment)
|
||||
fetchMerchantEquipments(merchantIdForEquipment)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user