first commit
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">总用户数</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ stats.totalUsers }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">今日订单</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ stats.todayOrders }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">今日收入</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.todayRevenue.toLocaleString() }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">活跃译员</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ stats.activeTranslators }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">最近活动</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="flow-root">
|
||||
<ul class="-my-5 divide-y divide-gray-200">
|
||||
<li v-for="activity in recentActivities" :key="activity.id" class="py-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div :class="activity.iconColor" class="w-8 h-8 rounded-full flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-white">{{ activity.icon }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">{{ activity.title }}</p>
|
||||
<p class="text-sm text-gray-500">{{ activity.description }}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 text-sm text-gray-500">{{ activity.time }}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 确保使用默认布局(包含Sidebar)和管理员认证
|
||||
definePageMeta({
|
||||
middleware: 'admin-auth', // 使用管理员认证中间件
|
||||
layout: 'default' // 明确指定使用默认布局
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '仪表板 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 临时注释掉Supabase导入
|
||||
// const { getStats, getCalls } = useSupabaseData()
|
||||
|
||||
// 当前用户信息
|
||||
const currentUser = ref({
|
||||
name: '系统管理员',
|
||||
role: '管理员'
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
totalUsers: 0,
|
||||
todayOrders: 0,
|
||||
todayRevenue: 0,
|
||||
activeTranslators: 0
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
|
||||
// 最近活动
|
||||
const recentActivities = ref([])
|
||||
|
||||
// 获取用户信息
|
||||
onMounted(() => {
|
||||
loadDashboardData()
|
||||
|
||||
// 客户端专用操作
|
||||
if (process.client) {
|
||||
const adminUser = localStorage.getItem('adminUser')
|
||||
if (adminUser) {
|
||||
try {
|
||||
const user = JSON.parse(adminUser)
|
||||
console.log('当前用户:', user)
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 加载仪表板数据
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
console.log('开始加载仪表板数据...')
|
||||
|
||||
// 临时注释掉Supabase调用,使用模拟数据
|
||||
// const statsData = await getStats()
|
||||
// console.log('统计数据加载成功:', statsData)
|
||||
|
||||
// 使用模拟数据
|
||||
const statsData = {
|
||||
totalUsers: 156,
|
||||
totalCalls: 23,
|
||||
totalRevenue: 12580,
|
||||
activeInterpreters: 8
|
||||
}
|
||||
|
||||
stats.value = {
|
||||
totalUsers: statsData.totalUsers || 0,
|
||||
todayOrders: statsData.totalCalls || 0,
|
||||
todayRevenue: statsData.totalRevenue || 0,
|
||||
activeTranslators: statsData.activeInterpreters || 0
|
||||
}
|
||||
|
||||
// 临时注释掉Supabase调用,使用模拟数据
|
||||
// const recentCalls = await getCalls()
|
||||
// console.log('通话记录加载成功:', recentCalls)
|
||||
|
||||
// 使用模拟活动数据
|
||||
recentActivities.value = [
|
||||
{
|
||||
id: 1,
|
||||
title: '新订单创建',
|
||||
description: '张先生 - 李译员',
|
||||
time: '2分钟前',
|
||||
icon: 'O',
|
||||
iconColor: 'bg-yellow-400'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '翻译完成',
|
||||
description: '王女士 - 陈译员',
|
||||
time: '15分钟前',
|
||||
icon: '✓',
|
||||
iconColor: 'bg-green-400'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '翻译进行中',
|
||||
description: '李总 - 刘译员',
|
||||
time: '30分钟前',
|
||||
icon: 'T',
|
||||
iconColor: 'bg-blue-400'
|
||||
}
|
||||
]
|
||||
|
||||
console.log('仪表板数据加载成功:', { stats: stats.value, activities: recentActivities.value })
|
||||
|
||||
} catch (err) {
|
||||
console.error('加载仪表板数据失败:', err)
|
||||
error.value = '加载数据失败,请刷新页面重试'
|
||||
// 显示默认数据
|
||||
stats.value = {
|
||||
totalUsers: 0,
|
||||
todayOrders: 0,
|
||||
todayRevenue: 0,
|
||||
activeTranslators: 0
|
||||
}
|
||||
recentActivities.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态获取活动标题
|
||||
const getActivityTitle = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '新订单创建',
|
||||
'in_progress': '翻译进行中',
|
||||
'completed': '翻译完成',
|
||||
'cancelled': '订单取消'
|
||||
}
|
||||
return statusMap[status] || '订单更新'
|
||||
}
|
||||
|
||||
// 根据状态获取活动图标
|
||||
const getActivityIcon = (status) => {
|
||||
const iconMap = {
|
||||
'pending': 'O',
|
||||
'in_progress': 'T',
|
||||
'completed': '✓',
|
||||
'cancelled': 'X'
|
||||
}
|
||||
return iconMap[status] || 'U'
|
||||
}
|
||||
|
||||
// 根据状态获取活动颜色
|
||||
const getActivityColor = (status) => {
|
||||
const colorMap = {
|
||||
'pending': 'bg-yellow-400',
|
||||
'in_progress': 'bg-blue-400',
|
||||
'completed': 'bg-green-400',
|
||||
'cancelled': 'bg-red-400'
|
||||
}
|
||||
return colorMap[status] || 'bg-gray-400'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return '未知时间'
|
||||
|
||||
const now = new Date()
|
||||
const time = new Date(timestamp)
|
||||
const diff = now - time
|
||||
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (minutes < 1) return '刚刚'
|
||||
if (minutes < 60) return `${minutes}分钟前`
|
||||
if (hours < 24) return `${hours}小时前`
|
||||
return `${days}天前`
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
<div class="p-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-6">系统诊断页面</h1>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- 布局测试 -->
|
||||
<div class="bg-white p-6 rounded-lg shadow">
|
||||
<h2 class="text-xl font-semibold mb-4">布局测试</h2>
|
||||
<div class="space-y-2 text-sm">
|
||||
<p><strong>当前页面:</strong> {{ $route.path }}</p>
|
||||
<p><strong>Tailwind样式测试:</strong>
|
||||
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded">蓝色徽章</span>
|
||||
</p>
|
||||
<p><strong>布局状态:</strong> {{ layoutStatus }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件测试 -->
|
||||
<div class="bg-white p-6 rounded-lg shadow">
|
||||
<h2 class="text-xl font-semibold mb-4">组件测试</h2>
|
||||
<div class="space-y-2 text-sm">
|
||||
<p><strong>Sidebar可见性:</strong> {{ sidebarVisible ? '可见' : '不可见' }}</p>
|
||||
<p><strong>用户认证:</strong> {{ isAuthenticated ? '已认证' : '未认证' }}</p>
|
||||
<p><strong>用户信息:</strong> {{ userInfo.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动Sidebar测试 -->
|
||||
<div class="bg-white p-6 rounded-lg shadow lg:col-span-2">
|
||||
<h2 class="text-xl font-semibold mb-4">手动Sidebar测试</h2>
|
||||
<p class="text-sm text-gray-600 mb-4">以下是手动嵌入的Sidebar组件:</p>
|
||||
<div class="border-2 border-dashed border-gray-300 p-4">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 不使用布局,完全手动控制
|
||||
definePageMeta({
|
||||
layout: false
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '系统诊断 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const layoutStatus = ref('手动控制')
|
||||
const sidebarVisible = ref(false)
|
||||
const isAuthenticated = ref(false)
|
||||
const userInfo = ref({
|
||||
name: '未知用户',
|
||||
role: '未知'
|
||||
})
|
||||
|
||||
// 检查Sidebar是否可见
|
||||
const checkSidebarVisibility = () => {
|
||||
if (process.client) {
|
||||
const sidebar = document.querySelector('.w-64.bg-gray-800')
|
||||
sidebarVisible.value = !!sidebar
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户信息
|
||||
const loadUserInfo = () => {
|
||||
if (process.client) {
|
||||
const authStatus = localStorage.getItem('isAuthenticated')
|
||||
const adminUser = localStorage.getItem('adminUser')
|
||||
|
||||
isAuthenticated.value = authStatus === 'true'
|
||||
|
||||
if (adminUser) {
|
||||
try {
|
||||
const user = JSON.parse(adminUser)
|
||||
userInfo.value = {
|
||||
name: user.name || '系统管理员',
|
||||
role: user.role || 'admin'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 页面挂载时检查
|
||||
onMounted(() => {
|
||||
loadUserInfo()
|
||||
checkSidebarVisibility()
|
||||
|
||||
// 延迟检查,确保组件完全渲染
|
||||
setTimeout(() => {
|
||||
checkSidebarVisibility()
|
||||
}, 1000)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 页面头部 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">财务管理</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">管理平台财务数据和交易记录</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
导出报表
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 财务统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">总收入</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.totalRevenue.toLocaleString() }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">本月收入</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.monthlyRevenue.toLocaleString() }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">待结算</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.pendingAmount.toLocaleString() }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">总支出</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.totalExpenses.toLocaleString() }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">搜索交易</label>
|
||||
<input
|
||||
id="search"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="订单号、用户名、备注"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="type-filter" class="block text-sm font-medium text-gray-700 mb-1">交易类型</label>
|
||||
<select
|
||||
id="type-filter"
|
||||
v-model="typeFilter"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">全部类型</option>
|
||||
<option value="income">收入</option>
|
||||
<option value="expense">支出</option>
|
||||
<option value="refund">退款</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="status-filter" class="block text-sm font-medium text-gray-700 mb-1">交易状态</label>
|
||||
<select
|
||||
id="status-filter"
|
||||
v-model="statusFilter"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="pending">待处理</option>
|
||||
<option value="failed">失败</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="date-range" class="block text-sm font-medium text-gray-700 mb-1">时间范围</label>
|
||||
<input
|
||||
id="date-range"
|
||||
v-model="dateRange"
|
||||
type="date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易记录列表 -->
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">交易记录</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">交易ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时间</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="transaction in filteredTransactions" :key="transaction.id" class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ transaction.id }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span :class="getTypeClass(transaction.type)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
|
||||
{{ getTypeName(transaction.type) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ transaction.userName }}</div>
|
||||
<div class="text-sm text-gray-500">{{ transaction.userEmail }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div :class="transaction.type === 'expense' ? 'text-red-600' : 'text-green-600'" class="text-sm font-medium">
|
||||
{{ transaction.type === 'expense' ? '-' : '+' }}¥{{ transaction.amount.toLocaleString() }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span :class="getStatusClass(transaction.status)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
|
||||
{{ getStatusName(transaction.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ formatDate(transaction.createdAt) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
@click="viewTransaction(transaction)"
|
||||
class="text-blue-600 hover:text-blue-900"
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
<button
|
||||
v-if="transaction.status === 'pending'"
|
||||
@click="processTransaction(transaction)"
|
||||
class="text-green-600 hover:text-green-900"
|
||||
>
|
||||
处理
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredTransactions.length === 0" class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">暂无交易记录</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">还没有任何财务交易记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 页面元数据
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
layout: 'default' // 明确指定使用默认布局
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '财务管理 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 导入Supabase数据操作
|
||||
const { getPayments } = useSupabaseData()
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 搜索和筛选
|
||||
const searchQuery = ref('')
|
||||
const typeFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
const dateRange = ref('')
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
totalRevenue: 0,
|
||||
monthlyRevenue: 0,
|
||||
pendingAmount: 0,
|
||||
totalExpenses: 0
|
||||
})
|
||||
|
||||
// 交易列表
|
||||
const transactions = ref([])
|
||||
const allTransactions = ref([])
|
||||
|
||||
// 计算属性:过滤后的交易
|
||||
const filteredTransactions = computed(() => {
|
||||
let filtered = transactions.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(transaction =>
|
||||
transaction.id.toLowerCase().includes(query) ||
|
||||
transaction.userName.toLowerCase().includes(query) ||
|
||||
transaction.userEmail.toLowerCase().includes(query) ||
|
||||
transaction.description.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
// 类型过滤
|
||||
if (typeFilter.value) {
|
||||
filtered = filtered.filter(transaction => transaction.type === typeFilter.value)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value) {
|
||||
filtered = filtered.filter(transaction => transaction.status === statusFilter.value)
|
||||
}
|
||||
|
||||
// 日期过滤
|
||||
if (dateRange.value) {
|
||||
const filterDate = new Date(dateRange.value)
|
||||
filtered = filtered.filter(transaction => {
|
||||
const transactionDate = new Date(transaction.createdAt)
|
||||
return transactionDate.toDateString() === filterDate.toDateString()
|
||||
})
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// 获取交易类型样式
|
||||
const getTypeClass = (type) => {
|
||||
const classes = {
|
||||
income: 'bg-green-100 text-green-800',
|
||||
expense: 'bg-red-100 text-red-800',
|
||||
refund: 'bg-yellow-100 text-yellow-800'
|
||||
}
|
||||
return classes[type] || 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
// 获取交易类型名称
|
||||
const getTypeName = (type) => {
|
||||
const names = {
|
||||
income: '收入',
|
||||
expense: '支出',
|
||||
refund: '退款'
|
||||
}
|
||||
return names[type] || type
|
||||
}
|
||||
|
||||
// 获取状态样式
|
||||
const getStatusClass = (status) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'bg-green-100 text-green-800'
|
||||
case 'pending':
|
||||
return 'bg-yellow-100 text-yellow-800'
|
||||
case 'failed':
|
||||
return 'bg-red-100 text-red-800'
|
||||
case 'refunded':
|
||||
return 'bg-gray-100 text-gray-800'
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态名称
|
||||
const getStatusName = (status) => {
|
||||
const statusMap = {
|
||||
completed: '已完成',
|
||||
pending: '待处理',
|
||||
failed: '失败',
|
||||
refunded: '已退款'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '未知时间'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 查看交易详情
|
||||
const viewTransaction = (transaction) => {
|
||||
alert(`交易ID: ${transaction.id}`)
|
||||
}
|
||||
|
||||
// 处理交易
|
||||
const processTransaction = async (transaction) => {
|
||||
if (confirm(`确定要处理交易 ${transaction.id} 吗?`)) {
|
||||
try {
|
||||
// 注意:这里需要实现Supabase的更新操作
|
||||
// 暂时模拟处理操作
|
||||
transaction.status = 'completed'
|
||||
updateStats()
|
||||
alert('交易处理成功')
|
||||
} catch (error) {
|
||||
alert('处理失败,请重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
const updateStats = () => {
|
||||
const now = new Date()
|
||||
const currentMonth = now.getMonth()
|
||||
const currentYear = now.getFullYear()
|
||||
|
||||
// 计算本月收入
|
||||
const monthlyIncome = allTransactions.value.filter(t => {
|
||||
const date = new Date(t.createdAt)
|
||||
return date.getMonth() === currentMonth &&
|
||||
date.getFullYear() === currentYear &&
|
||||
t.status === 'completed' &&
|
||||
t.amount > 0
|
||||
})
|
||||
|
||||
// 计算待处理金额
|
||||
const pending = allTransactions.value.filter(t => t.status === 'pending')
|
||||
|
||||
// 计算总费用(退款和其他支出)
|
||||
const expenses = allTransactions.value.filter(t =>
|
||||
t.status === 'completed' && (t.currency === 'refund' || t.amount < 0)
|
||||
)
|
||||
|
||||
stats.value = {
|
||||
totalRevenue: allTransactions.value.filter(t => t.status === 'completed' && t.amount > 0)
|
||||
.reduce((sum, t) => sum + t.amount, 0),
|
||||
monthlyRevenue: monthlyIncome.reduce((sum, t) => sum + t.amount, 0),
|
||||
pendingAmount: pending.filter(t => t.amount > 0).reduce((sum, t) => sum + t.amount, 0),
|
||||
totalExpenses: Math.abs(expenses.reduce((sum, t) => sum + Math.abs(t.amount), 0))
|
||||
}
|
||||
}
|
||||
|
||||
// 加载交易数据
|
||||
const loadTransactions = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 从Supabase获取支付记录
|
||||
const paymentsData = await getPayments()
|
||||
|
||||
// 转换数据格式以匹配财务显示
|
||||
allTransactions.value = paymentsData.map(payment => ({
|
||||
id: payment.id || `payment_${Date.now()}`,
|
||||
type: payment.amount > 0 ? 'income' : 'expense',
|
||||
amount: Math.abs(payment.amount),
|
||||
userName: payment.profiles?.full_name || '未知用户',
|
||||
userEmail: payment.profiles?.email || '未提供',
|
||||
status: payment.status || 'pending',
|
||||
description: getPaymentDescription(payment),
|
||||
createdAt: payment.created_at,
|
||||
currency: payment.currency || 'CNY',
|
||||
stripePaymentId: payment.stripe_payment_id
|
||||
}))
|
||||
|
||||
transactions.value = [...allTransactions.value]
|
||||
updateStats()
|
||||
|
||||
console.log('财务数据加载成功:', allTransactions.value.length, '笔交易')
|
||||
|
||||
} catch (err) {
|
||||
console.error('加载财务数据失败:', err)
|
||||
error.value = '加载数据失败,请刷新页面重试'
|
||||
allTransactions.value = []
|
||||
transactions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 根据支付信息生成描述
|
||||
const getPaymentDescription = (payment) => {
|
||||
if (payment.stripe_payment_id) {
|
||||
return `在线支付 - ${payment.currency || 'CNY'}`
|
||||
}
|
||||
if (payment.status === 'refunded') {
|
||||
return '订单退款'
|
||||
}
|
||||
return `支付交易 - ${payment.amount > 0 ? '收入' : '支出'}`
|
||||
}
|
||||
|
||||
// 页面挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadTransactions()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p class="mt-4 text-gray-600">{{ redirectMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 设置页面元数据
|
||||
definePageMeta({
|
||||
layout: false
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '翻译管理系统'
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const redirectMessage = ref('正在检查登录状态...')
|
||||
|
||||
// 智能跳转逻辑
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
// 检查本地存储的登录状态
|
||||
const isAuthenticated = localStorage.getItem('isAuthenticated')
|
||||
const adminUser = localStorage.getItem('adminUser')
|
||||
|
||||
if (isAuthenticated === 'true' && adminUser) {
|
||||
// 已登录,跳转到仪表板
|
||||
redirectMessage.value = '已登录,正在跳转到仪表板...'
|
||||
console.log('用户已登录,跳转到仪表板')
|
||||
navigateTo('/dashboard', { replace: true })
|
||||
} else {
|
||||
// 未登录,跳转到登录页
|
||||
redirectMessage.value = '未登录,正在跳转到登录页...'
|
||||
console.log('用户未登录,跳转到登录页')
|
||||
navigateTo('/login', { replace: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div class="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-blue-100">
|
||||
<svg class="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 002 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
管理员登录
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
请使用管理员账户登录系统
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="mt-8 space-y-6" @submit.prevent="handleLogin">
|
||||
<input type="hidden" name="remember" value="true">
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="username" class="sr-only">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="用户名/邮箱"
|
||||
v-model="loginForm.username"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="sr-only">密码</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="密码"
|
||||
v-model="loginForm.password"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="errorMessage" class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
登录失败
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<svg class="h-5 w-5 text-blue-500 group-hover:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-6 6c-3 0-5.5-1-5.5-1s2.5-1 5.5-1a6 6 0 016-6zM9 7a2 2 0 012 2m4 0a6 6 0 01-6 6c-3 0-5.5-1-5.5-1s2.5-1 5.5-1a6 6 0 016-6z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 测试账户信息 -->
|
||||
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||
<h4 class="text-sm font-medium text-blue-900 mb-2">测试账户信息</h4>
|
||||
<div class="text-xs text-blue-800 space-y-1">
|
||||
<p><strong>管理员账户:</strong>admin@example.com</p>
|
||||
<p><strong>密码:</strong>admin123</p>
|
||||
<p class="text-blue-600 mt-2">* 如果数据库中没有管理员账户,系统将自动创建</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 设置页面元数据,禁用默认布局
|
||||
definePageMeta({
|
||||
layout: false
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '管理员登录 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 导入认证函数
|
||||
const { login } = useAuth()
|
||||
|
||||
// 响应式数据
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 登录处理函数
|
||||
const handleLogin = async () => {
|
||||
// 清空之前的提示
|
||||
errorMessage.value = ''
|
||||
|
||||
// 验证输入
|
||||
if (!loginForm.value.username || !loginForm.value.password) {
|
||||
errorMessage.value = '请输入用户名和密码'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 使用真正的Supabase认证
|
||||
const user = await login(loginForm.value.username, loginForm.value.password)
|
||||
|
||||
if (user) {
|
||||
console.log('登录成功,用户信息:', user)
|
||||
// 跳转到仪表板
|
||||
await navigateTo('/dashboard', { replace: true })
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
errorMessage.value = error.message || '登录失败,请检查用户名和密码'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,924 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 页面头部 -->
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">订单管理</h1>
|
||||
<div class="mt-4 sm:mt-0">
|
||||
<button
|
||||
@click="openCreateModal"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
创建订单
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">总订单</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ stats.total }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">进行中</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ stats.inProgress }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">已完成</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ stats.completed }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">已取消</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ stats.cancelled }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">搜索订单</label>
|
||||
<input
|
||||
id="search"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="订单号、客户姓名、项目标题"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="status-filter" class="block text-sm font-medium text-gray-700 mb-1">订单状态</label>
|
||||
<select
|
||||
id="status-filter"
|
||||
v-model="statusFilter"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待处理</option>
|
||||
<option value="confirmed">已确认</option>
|
||||
<option value="in_progress">进行中</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="cancelled">已取消</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="service-filter" class="block text-sm font-medium text-gray-700 mb-1">服务类型</label>
|
||||
<select
|
||||
id="service-filter"
|
||||
v-model="serviceFilter"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">全部服务</option>
|
||||
<option value="voice">语音通话</option>
|
||||
<option value="video">视频通话</option>
|
||||
<option value="document">文档翻译</option>
|
||||
<option value="interpretation">口译服务</option>
|
||||
<option value="localization">本地化</option>
|
||||
<option value="proofreading">校对服务</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="date-range" class="block text-sm font-medium text-gray-700 mb-1">创建时间</label>
|
||||
<select
|
||||
id="date-range"
|
||||
v-model="dateRange"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">全部时间</option>
|
||||
<option value="today">今天</option>
|
||||
<option value="week">本周</option>
|
||||
<option value="month">本月</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">订单列表</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
订单信息
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
客户信息
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
项目信息
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
服务详情
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
状态
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
预估费用
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
口译员
|
||||
</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">操作</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="order in filteredOrders" :key="order.id" class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ order.order_number }}</div>
|
||||
<div class="text-sm text-gray-500">{{ formatDate(order.created_at) }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ order.client_name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ order.client_email }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ order.project_name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ getUrgencyText(order.urgency) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ getServiceTypeText(order.service_type) }}</div>
|
||||
<div class="text-sm text-gray-500">{{ getLanguageName(order.source_language) }} → {{ getLanguageName(order.target_language) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span :class="getStatusClass(order.status)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
|
||||
{{ getStatusText(order.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
¥{{ order.estimated_cost || 0 }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ order.interpreter_name || '未分配' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
@click="viewOrder(order)"
|
||||
class="text-blue-600 hover:text-blue-900 mr-3"
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
<button
|
||||
@click="editOrder(order)"
|
||||
class="text-green-600 hover:text-green-900 mr-3"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
@click="handleDeleteOrder(order)"
|
||||
class="text-red-600 hover:text-red-900"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredOrders.length === 0" class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">暂无订单</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">开始创建您的第一个翻译订单</p>
|
||||
<div class="mt-6">
|
||||
<button
|
||||
@click="openCreateModal"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
创建订单
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建订单模态框 -->
|
||||
<div v-if="showCreateModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-10 mx-auto p-5 border w-11/12 md:w-2/3 lg:w-1/2 shadow-lg rounded-md bg-white max-h-[90vh] overflow-y-auto">
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">创建新订单</h3>
|
||||
<button @click="showCreateModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitCreateOrder" class="space-y-6">
|
||||
<!-- 基本信息 -->
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-3">基本信息</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">客户姓名 *</label>
|
||||
<input
|
||||
v-model="newOrder.client_name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入客户姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">客户邮箱 *</label>
|
||||
<input
|
||||
v-model="newOrder.client_email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入客户邮箱"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">客户电话</label>
|
||||
<input
|
||||
v-model="newOrder.client_phone"
|
||||
type="tel"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入客户电话"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">项目标题 *</label>
|
||||
<input
|
||||
v-model="newOrder.project_name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入项目标题"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务详情 -->
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-3">服务详情</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">服务类型 *</label>
|
||||
<select
|
||||
v-model="newOrder.service_type"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">请选择服务类型</option>
|
||||
<option value="voice">语音通话</option>
|
||||
<option value="video">视频通话</option>
|
||||
<option value="document">文档翻译</option>
|
||||
<option value="interpretation">口译服务</option>
|
||||
<option value="localization">本地化</option>
|
||||
<option value="proofreading">校对服务</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">源语言 *</label>
|
||||
<select
|
||||
v-model="newOrder.source_language"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">请选择源语言</option>
|
||||
<option value="zh">中文</option>
|
||||
<option value="en">英文</option>
|
||||
<option value="ja">日文</option>
|
||||
<option value="ko">韩文</option>
|
||||
<option value="fr">法文</option>
|
||||
<option value="de">德文</option>
|
||||
<option value="es">西班牙文</option>
|
||||
<option value="ru">俄文</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">目标语言 *</label>
|
||||
<select
|
||||
v-model="newOrder.target_language"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">请选择目标语言</option>
|
||||
<option value="zh">中文</option>
|
||||
<option value="en">英文</option>
|
||||
<option value="ja">日文</option>
|
||||
<option value="ko">韩文</option>
|
||||
<option value="fr">法文</option>
|
||||
<option value="de">德文</option>
|
||||
<option value="es">西班牙文</option>
|
||||
<option value="ru">俄文</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">紧急程度 *</label>
|
||||
<select
|
||||
v-model="newOrder.urgency"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="normal">普通</option>
|
||||
<option value="urgent">紧急</option>
|
||||
<option value="emergency">特急</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目描述 -->
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-3">项目描述</h4>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">详细描述</label>
|
||||
<textarea
|
||||
v-model="newOrder.project_description"
|
||||
rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请详细描述项目需求、要求等..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预算信息 -->
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-3">预算信息</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">预估费用(元)</label>
|
||||
<input
|
||||
v-model.number="newOrder.estimated_cost"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">预计完成时间</label>
|
||||
<input
|
||||
v-model="newOrder.scheduled_date"
|
||||
type="date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户公司和预计时长 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">客户公司</label>
|
||||
<input
|
||||
v-model="newOrder.client_company"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="可选"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">预计时长(分钟)</label>
|
||||
<input
|
||||
v-model.number="newOrder.expected_duration"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预约时间 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">预约时间</label>
|
||||
<input
|
||||
v-model="newOrder.scheduled_time"
|
||||
type="time"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">备注</label>
|
||||
<textarea
|
||||
v-model="newOrder.notes"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="其他说明..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div id="estimated-cost" class="text-lg font-semibold text-gray-900">
|
||||
预估费用: ¥{{ calculateOrderCost(
|
||||
newOrder.service_type,
|
||||
newOrder.source_language,
|
||||
newOrder.target_language,
|
||||
newOrder.urgency,
|
||||
newOrder.expected_duration || 60
|
||||
) }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 mt-1">
|
||||
费用会根据服务类型、语言对、紧急程度和预计时长自动计算
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="showCreateModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
||||
>
|
||||
创建订单
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSupabaseData } from '~/composables/useSupabaseData'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
// 定义页面meta
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
// 使用Supabase数据操作
|
||||
const {
|
||||
getOrders,
|
||||
createOrder,
|
||||
updateOrder,
|
||||
deleteOrder,
|
||||
getOrderStats,
|
||||
calculateOrderCost
|
||||
} = useSupabaseData()
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const serviceFilter = ref('')
|
||||
const dateRange = ref('')
|
||||
const showCreateModal = ref(false)
|
||||
|
||||
// 订单数据
|
||||
const allOrders = ref([])
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
pending: 0,
|
||||
inProgress: 0,
|
||||
completed: 0,
|
||||
totalRevenue: 0
|
||||
})
|
||||
|
||||
// 新订单表单数据
|
||||
const newOrder = ref({
|
||||
client_name: '',
|
||||
client_email: '',
|
||||
client_phone: '',
|
||||
client_company: '',
|
||||
project_name: '',
|
||||
project_description: '',
|
||||
source_language: 'zh',
|
||||
target_language: 'en',
|
||||
service_type: 'audio',
|
||||
urgency: 'normal',
|
||||
expected_duration: 60,
|
||||
scheduled_date: '',
|
||||
scheduled_time: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const filteredOrders = computed(() => {
|
||||
let filtered = allOrders.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(order =>
|
||||
order.client_name?.toLowerCase().includes(query) ||
|
||||
order.client_email?.toLowerCase().includes(query) ||
|
||||
order.project_name?.toLowerCase().includes(query) ||
|
||||
order.order_number?.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value) {
|
||||
filtered = filtered.filter(order => order.status === statusFilter.value)
|
||||
}
|
||||
|
||||
// 服务类型过滤
|
||||
if (serviceFilter.value) {
|
||||
filtered = filtered.filter(order => order.service_type === serviceFilter.value)
|
||||
}
|
||||
|
||||
// 日期范围过滤
|
||||
if (dateRange.value) {
|
||||
const today = new Date()
|
||||
const filterDate = new Date()
|
||||
|
||||
switch (dateRange.value) {
|
||||
case 'today':
|
||||
filterDate.setHours(0, 0, 0, 0)
|
||||
filtered = filtered.filter(order =>
|
||||
new Date(order.created_at) >= filterDate
|
||||
)
|
||||
break
|
||||
case 'week':
|
||||
filterDate.setDate(today.getDate() - 7)
|
||||
filtered = filtered.filter(order =>
|
||||
new Date(order.created_at) >= filterDate
|
||||
)
|
||||
break
|
||||
case 'month':
|
||||
filterDate.setMonth(today.getMonth() - 1)
|
||||
filtered = filtered.filter(order =>
|
||||
new Date(order.created_at) >= filterDate
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
pending: '待确认',
|
||||
confirmed: '已确认',
|
||||
in_progress: '进行中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status) => {
|
||||
const classMap = {
|
||||
pending: 'bg-yellow-100 text-yellow-800',
|
||||
confirmed: 'bg-blue-100 text-blue-800',
|
||||
in_progress: 'bg-green-100 text-green-800',
|
||||
completed: 'bg-green-100 text-green-800',
|
||||
cancelled: 'bg-red-100 text-red-800'
|
||||
}
|
||||
return classMap[status] || 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
// 获取服务类型文本
|
||||
const getServiceTypeText = (type) => {
|
||||
const typeMap = {
|
||||
voice: '语音通话',
|
||||
video: '视频通话',
|
||||
document: '文档翻译',
|
||||
interpretation: '口译服务',
|
||||
localization: '本地化',
|
||||
proofreading: '校对服务',
|
||||
// 向后兼容
|
||||
audio: '语音翻译',
|
||||
text: '文本翻译'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取紧急程度文本
|
||||
const getUrgencyText = (urgency) => {
|
||||
const urgencyMap = {
|
||||
normal: '普通',
|
||||
urgent: '紧急',
|
||||
emergency: '特急'
|
||||
}
|
||||
return urgencyMap[urgency] || urgency
|
||||
}
|
||||
|
||||
// 获取语言名称
|
||||
const getLanguageName = (code) => {
|
||||
const languages = {
|
||||
zh: '中文',
|
||||
en: '英文',
|
||||
ja: '日文',
|
||||
ko: '韩文',
|
||||
fr: '法文',
|
||||
de: '德文',
|
||||
es: '西班牙文',
|
||||
ru: '俄文'
|
||||
}
|
||||
return languages[code] || code
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '未知时间'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
const viewOrder = (order) => {
|
||||
// 可以跳转到订单详情页面
|
||||
console.log('查看订单:', order)
|
||||
showToast('订单详情功能开发中', 'info')
|
||||
}
|
||||
|
||||
// 编辑订单
|
||||
const editOrder = (order) => {
|
||||
// 可以打开编辑模态框或跳转到编辑页面
|
||||
console.log('编辑订单:', order)
|
||||
showToast('编辑订单功能开发中', 'info')
|
||||
}
|
||||
|
||||
// 删除订单
|
||||
const handleDeleteOrder = async (order) => {
|
||||
if (confirm(`确定要删除订单 ${order.order_number} 吗?`)) {
|
||||
try {
|
||||
const success = await deleteOrder(order.id)
|
||||
|
||||
if (success) {
|
||||
// 从列表中移除
|
||||
const index = allOrders.value.findIndex(o => o.id === order.id)
|
||||
if (index > -1) {
|
||||
allOrders.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 更新统计
|
||||
stats.value.total--
|
||||
if (order.status === 'pending') stats.value.pending--
|
||||
else if (order.status === 'in_progress') stats.value.inProgress--
|
||||
else if (order.status === 'completed') stats.value.completed--
|
||||
|
||||
showToast('订单删除成功', 'success')
|
||||
} else {
|
||||
throw new Error('删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除订单失败:', error)
|
||||
showToast('删除订单失败', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 方法
|
||||
const loadOrders = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 从数据库加载真实数据
|
||||
const [ordersData, statsData] = await Promise.all([
|
||||
getOrders(),
|
||||
getOrderStats()
|
||||
])
|
||||
|
||||
allOrders.value = ordersData || []
|
||||
stats.value = statsData || {
|
||||
total: 0,
|
||||
pending: 0,
|
||||
inProgress: 0,
|
||||
completed: 0,
|
||||
cancelled: 0,
|
||||
totalRevenue: 0
|
||||
}
|
||||
|
||||
console.log('成功加载订单数据:', ordersData?.length || 0, '条订单')
|
||||
} catch (err) {
|
||||
console.error('加载订单数据失败:', err)
|
||||
error.value = '加载订单数据失败'
|
||||
showToast('加载订单数据失败,请稍后重试', 'error')
|
||||
|
||||
// 初始化空数据
|
||||
allOrders.value = []
|
||||
stats.value = {
|
||||
total: 0,
|
||||
pending: 0,
|
||||
inProgress: 0,
|
||||
completed: 0,
|
||||
cancelled: 0,
|
||||
totalRevenue: 0
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openCreateModal = () => {
|
||||
// 重置表单
|
||||
newOrder.value = {
|
||||
client_name: '',
|
||||
client_email: '',
|
||||
client_phone: '',
|
||||
client_company: '',
|
||||
project_name: '',
|
||||
project_description: '',
|
||||
source_language: 'zh',
|
||||
target_language: 'en',
|
||||
service_type: 'audio',
|
||||
urgency: 'normal',
|
||||
expected_duration: 60,
|
||||
scheduled_date: '',
|
||||
scheduled_time: '',
|
||||
notes: ''
|
||||
}
|
||||
showCreateModal.value = true
|
||||
}
|
||||
|
||||
const closeCreateModal = () => {
|
||||
showCreateModal.value = false
|
||||
}
|
||||
|
||||
const submitCreateOrder = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 表单验证
|
||||
if (!newOrder.value.client_name || !newOrder.value.client_email || !newOrder.value.project_name) {
|
||||
throw new Error('请填写必填信息')
|
||||
}
|
||||
|
||||
// 计算预估费用
|
||||
const estimatedCost = calculateOrderCost(
|
||||
newOrder.value.service_type,
|
||||
newOrder.value.source_language,
|
||||
newOrder.value.target_language,
|
||||
newOrder.value.urgency,
|
||||
newOrder.value.expected_duration
|
||||
)
|
||||
|
||||
// 创建订单
|
||||
const orderData = {
|
||||
...newOrder.value,
|
||||
estimated_cost: estimatedCost
|
||||
}
|
||||
|
||||
const createdOrder = await createOrder(orderData)
|
||||
|
||||
if (createdOrder) {
|
||||
// 添加到订单列表
|
||||
allOrders.value.unshift(createdOrder)
|
||||
|
||||
// 更新统计
|
||||
stats.value.total++
|
||||
stats.value.pending++
|
||||
|
||||
showToast('订单创建成功', 'success')
|
||||
closeCreateModal()
|
||||
} else {
|
||||
throw new Error('创建订单失败')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建订单失败:', err)
|
||||
showToast(err.message || '创建订单失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重新计算费用
|
||||
const recalculateCost = () => {
|
||||
if (newOrder.value.service_type && newOrder.value.source_language &&
|
||||
newOrder.value.target_language && newOrder.value.urgency) {
|
||||
const cost = calculateOrderCost(
|
||||
newOrder.value.service_type,
|
||||
newOrder.value.source_language,
|
||||
newOrder.value.target_language,
|
||||
newOrder.value.urgency,
|
||||
newOrder.value.expected_duration || 60
|
||||
)
|
||||
|
||||
// 显示计算出的费用
|
||||
nextTick(() => {
|
||||
const costElement = document.querySelector('#estimated-cost')
|
||||
if (costElement) {
|
||||
costElement.textContent = `预估费用: ¥${cost}`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 监听表单字段变化,自动重新计算费用
|
||||
watch([
|
||||
() => newOrder.value.service_type,
|
||||
() => newOrder.value.source_language,
|
||||
() => newOrder.value.target_language,
|
||||
() => newOrder.value.urgency,
|
||||
() => newOrder.value.expected_duration
|
||||
], recalculateCost)
|
||||
|
||||
// 页面挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadOrders()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 页面头部 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">创建订单</h1>
|
||||
<p class="text-gray-600 mt-1">创建新的翻译订单</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
@click="$router.back()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单表单 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<form @submit.prevent="handleSubmit" class="p-6 space-y-6">
|
||||
<!-- 客户信息 -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">客户信息</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
客户姓名 *
|
||||
</label>
|
||||
<input
|
||||
id="clientName"
|
||||
v-model="orderForm.clientName"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入客户姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="clientPhone" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
联系电话 *
|
||||
</label>
|
||||
<input
|
||||
id="clientPhone"
|
||||
v-model="orderForm.clientPhone"
|
||||
type="tel"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="clientEmail" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
邮箱地址
|
||||
</label>
|
||||
<input
|
||||
id="clientEmail"
|
||||
v-model="orderForm.clientEmail"
|
||||
type="email"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入邮箱地址"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="clientCompany" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
公司名称
|
||||
</label>
|
||||
<input
|
||||
id="clientCompany"
|
||||
v-model="orderForm.clientCompany"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入公司名称"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 翻译需求 -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">翻译需求</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="sourceLanguage" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
源语言 *
|
||||
</label>
|
||||
<select
|
||||
id="sourceLanguage"
|
||||
v-model="orderForm.sourceLanguage"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">请选择源语言</option>
|
||||
<option value="zh">中文</option>
|
||||
<option value="en">英文</option>
|
||||
<option value="ja">日文</option>
|
||||
<option value="ko">韩文</option>
|
||||
<option value="fr">法文</option>
|
||||
<option value="de">德文</option>
|
||||
<option value="es">西班牙文</option>
|
||||
<option value="ru">俄文</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="targetLanguage" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
目标语言 *
|
||||
</label>
|
||||
<select
|
||||
id="targetLanguage"
|
||||
v-model="orderForm.targetLanguage"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">请选择目标语言</option>
|
||||
<option value="zh">中文</option>
|
||||
<option value="en">英文</option>
|
||||
<option value="ja">日文</option>
|
||||
<option value="ko">韩文</option>
|
||||
<option value="fr">法文</option>
|
||||
<option value="de">德文</option>
|
||||
<option value="es">西班牙文</option>
|
||||
<option value="ru">俄文</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="serviceType" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
服务类型 *
|
||||
</label>
|
||||
<select
|
||||
id="serviceType"
|
||||
v-model="orderForm.serviceType"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">请选择服务类型</option>
|
||||
<option value="document">文档翻译</option>
|
||||
<option value="interpretation">口译服务</option>
|
||||
<option value="localization">本地化</option>
|
||||
<option value="proofreading">校对服务</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="urgency" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
紧急程度 *
|
||||
</label>
|
||||
<select
|
||||
id="urgency"
|
||||
v-model="orderForm.urgency"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="normal">普通</option>
|
||||
<option value="urgent">紧急</option>
|
||||
<option value="rush">加急</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目详情 -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">项目详情</h3>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="projectTitle" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
项目标题 *
|
||||
</label>
|
||||
<input
|
||||
id="projectTitle"
|
||||
v-model="orderForm.projectTitle"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入项目标题"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="projectDescription" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
项目描述 *
|
||||
</label>
|
||||
<textarea
|
||||
id="projectDescription"
|
||||
v-model="orderForm.projectDescription"
|
||||
rows="4"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请详细描述翻译需求、专业领域、特殊要求等"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="expectedDelivery" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
期望交付时间 *
|
||||
</label>
|
||||
<input
|
||||
id="expectedDelivery"
|
||||
v-model="orderForm.expectedDelivery"
|
||||
type="datetime-local"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="budget" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
预算(元)
|
||||
</label>
|
||||
<input
|
||||
id="budget"
|
||||
v-model.number="orderForm.budget"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入预算金额"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 附件上传 -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">附件上传</h3>
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="mt-4">
|
||||
<label for="file-upload" class="cursor-pointer">
|
||||
<span class="mt-2 block text-sm font-medium text-gray-900">
|
||||
点击上传文件或拖拽文件到此处
|
||||
</span>
|
||||
<input id="file-upload" name="file-upload" type="file" class="sr-only" multiple @change="handleFileUpload" />
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
支持 PDF, DOC, DOCX, TXT 等格式,单个文件最大 10MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已上传文件列表 -->
|
||||
<div v-if="uploadedFiles.length > 0" class="mt-4">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-2">已上传文件</h4>
|
||||
<ul class="space-y-2">
|
||||
<li v-for="(file, index) in uploadedFiles" :key="index" class="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||
<span class="text-sm text-gray-700">{{ file.name }}</span>
|
||||
<button @click="removeFile(index)" class="text-red-600 hover:text-red-800">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<div>
|
||||
<label for="notes" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
备注信息
|
||||
</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
v-model="orderForm.notes"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="其他需要说明的信息"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div class="flex justify-end space-x-3 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
@click="$router.back()"
|
||||
class="px-6 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isSubmitting"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ isSubmitting ? '创建中...' : '创建订单' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 页面元数据
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '创建订单 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 表单数据
|
||||
const orderForm = ref({
|
||||
clientName: '',
|
||||
clientPhone: '',
|
||||
clientEmail: '',
|
||||
clientCompany: '',
|
||||
sourceLanguage: '',
|
||||
targetLanguage: '',
|
||||
serviceType: '',
|
||||
urgency: 'normal',
|
||||
projectTitle: '',
|
||||
projectDescription: '',
|
||||
expectedDelivery: '',
|
||||
budget: 0,
|
||||
notes: ''
|
||||
})
|
||||
|
||||
// 上传文件列表
|
||||
const uploadedFiles = ref([])
|
||||
|
||||
// 提交状态
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileUpload = (event) => {
|
||||
const files = Array.from(event.target.files)
|
||||
files.forEach(file => {
|
||||
if (file.size <= 10 * 1024 * 1024) { // 10MB限制
|
||||
uploadedFiles.value.push(file)
|
||||
} else {
|
||||
alert(`文件 ${file.name} 超过10MB限制`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
const removeFile = (index) => {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 处理表单提交
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
isSubmitting.value = true
|
||||
|
||||
// 基本验证
|
||||
if (!orderForm.value.clientName || !orderForm.value.clientPhone ||
|
||||
!orderForm.value.sourceLanguage || !orderForm.value.targetLanguage ||
|
||||
!orderForm.value.serviceType || !orderForm.value.projectTitle ||
|
||||
!orderForm.value.projectDescription || !orderForm.value.expectedDelivery) {
|
||||
throw new Error('请填写所有必填字段')
|
||||
}
|
||||
|
||||
// 语言验证
|
||||
if (orderForm.value.sourceLanguage === orderForm.value.targetLanguage) {
|
||||
throw new Error('源语言和目标语言不能相同')
|
||||
}
|
||||
|
||||
// 时间验证
|
||||
const deliveryTime = new Date(orderForm.value.expectedDelivery)
|
||||
const now = new Date()
|
||||
if (deliveryTime <= now) {
|
||||
throw new Error('交付时间必须晚于当前时间')
|
||||
}
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 创建订单数据
|
||||
const newOrder = {
|
||||
id: `order_${Date.now()}`,
|
||||
...orderForm.value,
|
||||
files: uploadedFiles.value.map(file => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
})),
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
console.log('创建订单:', newOrder)
|
||||
|
||||
// 显示成功消息
|
||||
alert('订单创建成功')
|
||||
|
||||
// 返回订单列表页面
|
||||
router.push('/orders')
|
||||
|
||||
} catch (error) {
|
||||
alert(error.message || '创建订单失败,请重试')
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 页面头部 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">数据报表</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">查看平台运营数据和业务报表</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
@click="exportReport"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
||||
>
|
||||
导出报表
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间范围选择 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label for="start-date" class="block text-sm font-medium text-gray-700 mb-1">开始日期</label>
|
||||
<input
|
||||
id="start-date"
|
||||
v-model="startDate"
|
||||
type="date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="end-date" class="block text-sm font-medium text-gray-700 mb-1">结束日期</label>
|
||||
<input
|
||||
id="end-date"
|
||||
v-model="endDate"
|
||||
type="date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="report-type" class="block text-sm font-medium text-gray-700 mb-1">报表类型</label>
|
||||
<select
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="overview">综合概览</option>
|
||||
<option value="orders">订单报表</option>
|
||||
<option value="users">用户报表</option>
|
||||
<option value="finance">财务报表</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
@click="generateReport"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700"
|
||||
>
|
||||
生成报表
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据概览卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">总订单数</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ reportData.totalOrders.toLocaleString() }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">总收入</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">¥{{ reportData.totalRevenue.toLocaleString() }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">活跃用户</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ reportData.activeUsers.toLocaleString() }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">完成率</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ reportData.completionRate }}%</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- 订单趋势图 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">订单趋势</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="h-64 bg-gray-50 rounded-lg flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500">订单趋势图表</p>
|
||||
<p class="text-xs text-gray-400">可集成 Chart.js 或其他图表库</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收入分析图 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">收入分析</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="h-64 bg-gray-50 rounded-lg flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"/>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500">收入分析图表</p>
|
||||
<p class="text-xs text-gray-400">可集成 Chart.js 或其他图表库</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细报表数据 -->
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">详细数据</h3>
|
||||
</div>
|
||||
|
||||
<!-- 订单报表 -->
|
||||
<div v-if="reportType === 'orders'" class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">新增订单</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">完成订单</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">取消订单</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">完成率</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="row in orderReportData" :key="row.date" class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ row.date }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ row.newOrders }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">{{ row.completedOrders }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-red-600">{{ row.cancelledOrders }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ row.completionRate }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 用户报表 -->
|
||||
<div v-else-if="reportType === 'users'" class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">新增用户</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">活跃用户</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">留存率</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="row in userReportData" :key="row.date" class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ row.date }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">{{ row.newUsers }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">{{ row.activeUsers }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ row.retentionRate }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 财务报表 -->
|
||||
<div v-else-if="reportType === 'finance'" class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">收入</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">支出</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">净利润</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="row in financeReportData" :key="row.date" class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ row.date }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">¥{{ row.revenue.toLocaleString() }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-red-600">¥{{ row.expenses.toLocaleString() }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium" :class="row.profit >= 0 ? 'text-green-600' : 'text-red-600'">
|
||||
¥{{ row.profit.toLocaleString() }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 综合概览 -->
|
||||
<div v-else class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-2">订单统计</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">总订单数:</span>
|
||||
<span class="font-medium">{{ reportData.totalOrders }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">已完成:</span>
|
||||
<span class="font-medium text-green-600">{{ reportData.completedOrders }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">进行中:</span>
|
||||
<span class="font-medium text-blue-600">{{ reportData.pendingOrders }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-2">用户统计</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">总用户数:</span>
|
||||
<span class="font-medium">{{ reportData.totalUsers }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">活跃用户:</span>
|
||||
<span class="font-medium text-green-600">{{ reportData.activeUsers }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">新增用户:</span>
|
||||
<span class="font-medium text-blue-600">{{ reportData.newUsers }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-2">财务统计</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">总收入:</span>
|
||||
<span class="font-medium text-green-600">¥{{ reportData.totalRevenue.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">总支出:</span>
|
||||
<span class="font-medium text-red-600">¥{{ reportData.totalExpenses.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">净利润:</span>
|
||||
<span class="font-medium" :class="reportData.netProfit >= 0 ? 'text-green-600' : 'text-red-600'">
|
||||
¥{{ reportData.netProfit.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!reportData.totalOrders" class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">暂无报表数据</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">请选择日期范围并生成报表</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 页面元数据 - 确保使用默认布局
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
layout: 'default' // 明确指定使用默认布局
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '数据报表 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 日期范围
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
const reportType = ref('overview')
|
||||
|
||||
// 报表数据
|
||||
const reportData = ref({
|
||||
totalOrders: 0,
|
||||
totalRevenue: 0,
|
||||
activeUsers: 0,
|
||||
completionRate: 0,
|
||||
completedOrders: 0,
|
||||
pendingOrders: 0,
|
||||
totalUsers: 0,
|
||||
newUsers: 0,
|
||||
totalExpenses: 0,
|
||||
netProfit: 0
|
||||
})
|
||||
|
||||
// 订单报表数据
|
||||
const orderReportData = ref([])
|
||||
|
||||
// 用户报表数据
|
||||
const userReportData = ref([])
|
||||
|
||||
// 财务报表数据
|
||||
const financeReportData = ref([])
|
||||
|
||||
// 生成报表
|
||||
const generateReport = async () => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟报表数据
|
||||
reportData.value = {
|
||||
totalOrders: 1250,
|
||||
totalRevenue: 850000,
|
||||
activeUsers: 320,
|
||||
completionRate: 92.5,
|
||||
completedOrders: 1156,
|
||||
pendingOrders: 94,
|
||||
totalUsers: 450,
|
||||
newUsers: 28,
|
||||
totalExpenses: 320000,
|
||||
netProfit: 530000
|
||||
}
|
||||
|
||||
// 根据报表类型生成相应数据
|
||||
if (reportType.value === 'orders') {
|
||||
orderReportData.value = [
|
||||
{ date: '2024-01-15', newOrders: 45, completedOrders: 42, cancelledOrders: 2, completionRate: 93.3 },
|
||||
{ date: '2024-01-14', newOrders: 38, completedOrders: 35, cancelledOrders: 1, completionRate: 92.1 },
|
||||
{ date: '2024-01-13', newOrders: 52, completedOrders: 48, cancelledOrders: 3, completionRate: 92.3 },
|
||||
{ date: '2024-01-12', newOrders: 41, completedOrders: 39, cancelledOrders: 1, completionRate: 95.1 },
|
||||
{ date: '2024-01-11', newOrders: 47, completedOrders: 44, cancelledOrders: 2, completionRate: 93.6 }
|
||||
]
|
||||
} else if (reportType.value === 'users') {
|
||||
userReportData.value = [
|
||||
{ date: '2024-01-15', newUsers: 12, activeUsers: 89, retentionRate: 85.2 },
|
||||
{ date: '2024-01-14', newUsers: 8, activeUsers: 92, retentionRate: 87.1 },
|
||||
{ date: '2024-01-13', newUsers: 15, activeUsers: 95, retentionRate: 84.6 },
|
||||
{ date: '2024-01-12', newUsers: 10, activeUsers: 88, retentionRate: 86.3 },
|
||||
{ date: '2024-01-11', newUsers: 14, activeUsers: 91, retentionRate: 85.8 }
|
||||
]
|
||||
} else if (reportType.value === 'finance') {
|
||||
financeReportData.value = [
|
||||
{ date: '2024-01-15', revenue: 45000, expenses: 18000, profit: 27000 },
|
||||
{ date: '2024-01-14', revenue: 38000, expenses: 15000, profit: 23000 },
|
||||
{ date: '2024-01-13', revenue: 52000, expenses: 21000, profit: 31000 },
|
||||
{ date: '2024-01-12', revenue: 41000, expenses: 16000, profit: 25000 },
|
||||
{ date: '2024-01-11', revenue: 47000, expenses: 19000, profit: 28000 }
|
||||
]
|
||||
}
|
||||
|
||||
alert('报表生成成功!')
|
||||
} catch (error) {
|
||||
alert('生成报表失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出报表
|
||||
const exportReport = () => {
|
||||
// 这里可以实现导出功能
|
||||
alert('导出功能开发中...')
|
||||
}
|
||||
|
||||
// 初始化日期
|
||||
onMounted(() => {
|
||||
const today = new Date()
|
||||
const lastWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
|
||||
endDate.value = today.toISOString().split('T')[0]
|
||||
startDate.value = lastWeek.toISOString().split('T')[0]
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<div class="space-y-8">
|
||||
<!-- 页面头部 -->
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">系统设置</h1>
|
||||
<p class="mt-1 text-sm text-gray-600">管理系统的基本配置和参数</p>
|
||||
</div>
|
||||
|
||||
<!-- 基本设置 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">基本设置</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">系统名称</label>
|
||||
<input
|
||||
v-model="settings.systemName"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入系统名称"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">系统描述</label>
|
||||
<input
|
||||
v-model="settings.systemDescription"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入系统描述"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">管理员邮箱</label>
|
||||
<input
|
||||
v-model="settings.adminEmail"
|
||||
type="email"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入管理员邮箱"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">联系电话</label>
|
||||
<input
|
||||
v-model="settings.contactPhone"
|
||||
type="tel"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 翻译设置 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">翻译设置</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">默认翻译费率(元/字)</label>
|
||||
<input
|
||||
v-model.number="settings.defaultRate"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入默认费率"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">最低充值金额(元)</label>
|
||||
<input
|
||||
v-model.number="settings.minRecharge"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入最低充值金额"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">支持的语言对</label>
|
||||
<select
|
||||
v-model="settings.supportedLanguages"
|
||||
multiple
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="zh-en">中文-英文</option>
|
||||
<option value="zh-ja">中文-日文</option>
|
||||
<option value="zh-ko">中文-韩文</option>
|
||||
<option value="en-ja">英文-日文</option>
|
||||
<option value="en-ko">英文-韩文</option>
|
||||
<option value="ja-ko">日文-韩文</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">质量等级</label>
|
||||
<select
|
||||
v-model="settings.qualityLevel"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="standard">标准</option>
|
||||
<option value="professional">专业</option>
|
||||
<option value="premium">高级</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户管理设置 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">用户管理设置</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">新用户默认余额(元)</label>
|
||||
<input
|
||||
v-model.number="settings.defaultBalance"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入默认余额"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">用户注册审核</label>
|
||||
<select
|
||||
v-model="settings.registrationApproval"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="auto">自动通过</option>
|
||||
<option value="manual">手动审核</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">会话超时时间(分钟)</label>
|
||||
<input
|
||||
v-model.number="settings.sessionTimeout"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入超时时间"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">密码最小长度</label>
|
||||
<input
|
||||
v-model.number="settings.minPasswordLength"
|
||||
type="number"
|
||||
min="6"
|
||||
max="20"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入最小长度"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统维护 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">系统维护</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900">维护模式</h4>
|
||||
<p class="text-sm text-gray-500">启用后系统将进入维护状态</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="settings.maintenanceMode"
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900">自动备份</h4>
|
||||
<p class="text-sm text-gray-500">每日自动备份数据库</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="settings.autoBackup"
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900">邮件通知</h4>
|
||||
<p class="text-sm text-gray-500">系统事件邮件提醒</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="settings.emailNotifications"
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button
|
||||
@click="resetSettings"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
@click="saveSettings"
|
||||
:disabled="saving"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{{ saving ? '保存中...' : '保存设置' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 页面元数据 - 确保使用默认布局
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
layout: 'default' // 明确指定使用默认布局
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '系统设置 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const saving = ref(false)
|
||||
|
||||
// 设置数据
|
||||
const settings = ref({
|
||||
// 基本设置
|
||||
systemName: '翻译管理系统',
|
||||
systemDescription: '专业的翻译服务管理平台',
|
||||
adminEmail: 'admin@system.com',
|
||||
contactPhone: '400-123-4567',
|
||||
|
||||
// 翻译设置
|
||||
defaultRate: 0.15,
|
||||
minRecharge: 100,
|
||||
supportedLanguages: ['zh-en', 'zh-ja'],
|
||||
qualityLevel: 'professional',
|
||||
|
||||
// 用户管理设置
|
||||
defaultBalance: 0,
|
||||
registrationApproval: 'auto',
|
||||
sessionTimeout: 30,
|
||||
minPasswordLength: 8,
|
||||
|
||||
// 系统维护
|
||||
maintenanceMode: false,
|
||||
autoBackup: true,
|
||||
emailNotifications: true
|
||||
})
|
||||
|
||||
// 默认设置(用于重置)
|
||||
const defaultSettings = {
|
||||
systemName: '翻译管理系统',
|
||||
systemDescription: '专业的翻译服务管理平台',
|
||||
adminEmail: 'admin@system.com',
|
||||
contactPhone: '400-123-4567',
|
||||
defaultRate: 0.15,
|
||||
minRecharge: 100,
|
||||
supportedLanguages: ['zh-en', 'zh-ja'],
|
||||
qualityLevel: 'professional',
|
||||
defaultBalance: 0,
|
||||
registrationApproval: 'auto',
|
||||
sessionTimeout: 30,
|
||||
minPasswordLength: 8,
|
||||
maintenanceMode: false,
|
||||
autoBackup: true,
|
||||
emailNotifications: true
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const saveSettings = () => {
|
||||
try {
|
||||
if (process.client) {
|
||||
localStorage.setItem('systemSettings', JSON.stringify(settings.value))
|
||||
}
|
||||
|
||||
ElMessage.success('设置保存成功')
|
||||
console.log('设置已保存:', settings.value)
|
||||
} catch (error) {
|
||||
console.error('保存设置失败:', error)
|
||||
ElMessage.error('保存设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
const loadSettings = () => {
|
||||
try {
|
||||
if (process.client) {
|
||||
const savedSettings = localStorage.getItem('systemSettings')
|
||||
if (savedSettings) {
|
||||
const parsed = JSON.parse(savedSettings)
|
||||
settings.value = { ...settings.value, ...parsed }
|
||||
console.log('设置已加载:', settings.value)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置设置
|
||||
const resetSettings = () => {
|
||||
if (confirm('确定要重置所有设置到默认值吗?')) {
|
||||
settings.value = { ...defaultSettings }
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载设置
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
loadSettings()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
+924
@@ -0,0 +1,924 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 页面头部 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">用户管理</h1>
|
||||
<p class="mt-1 text-sm text-gray-600">管理系统中的所有用户账户</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
@click="exportUsers"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
导出数据
|
||||
</button>
|
||||
<button
|
||||
@click="createUser"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
||||
>
|
||||
添加用户
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white text-sm font-semibold">总</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">总用户数</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ userStats.total }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white text-sm font-semibold">译</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">译员数量</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ userStats.interpreters }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white text-sm font-semibold">管</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">管理员数量</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ userStats.admins }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和过滤 -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">搜索用户</label>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="搜索姓名、邮箱或手机号"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">角色筛选</label>
|
||||
<select
|
||||
v-model="selectedRole"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">全部角色</option>
|
||||
<option value="admin">管理员</option>
|
||||
<option value="customer">客户</option>
|
||||
<option value="interpreter">翻译员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">状态筛选</label>
|
||||
<select
|
||||
v-model="selectedStatus"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="active">活跃</option>
|
||||
<option value="inactive">非活跃</option>
|
||||
<option value="suspended">已暂停</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
@click="resetFilters"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200"
|
||||
>
|
||||
重置筛选
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
用户
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
角色
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
状态
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
余额
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
注册时间
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="user in paginatedUsers" :key="user.id">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
{{ user.full_name?.charAt(0) || '用' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">{{ user.full_name || '未设置' }}</div>
|
||||
<div class="text-sm text-gray-500">{{ user.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
||||
:class="{
|
||||
'bg-purple-100 text-purple-800': user.role === 'admin',
|
||||
'bg-green-100 text-green-800': user.role === 'customer',
|
||||
'bg-blue-100 text-blue-800': user.role === 'interpreter'
|
||||
}">
|
||||
{{ getRoleText(user.role) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
||||
:class="getStatusClass(user.status)">
|
||||
{{ getStatusText(user.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
¥{{ user.credits?.toFixed(2) || '0.00' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ formatDate(user.created_at) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
@click="editUser(user)"
|
||||
class="text-blue-600 hover:text-blue-900 mr-3"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
@click="deleteUser(user)"
|
||||
class="text-red-600 hover:text-red-900"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6" v-if="totalPages > 1">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<button
|
||||
@click="goToPage(currentPage - 1)"
|
||||
:disabled="currentPage <= 1"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
@click="goToPage(currentPage + 1)"
|
||||
:disabled="currentPage >= totalPages"
|
||||
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">
|
||||
显示第 <span class="font-medium">{{ (currentPage - 1) * 20 + 1 }}</span> 到
|
||||
<span class="font-medium">{{ Math.min(currentPage * 20, filteredUsers.length) }}</span> 条,
|
||||
共 <span class="font-medium">{{ filteredUsers.length }}</span> 条记录
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||||
<button
|
||||
@click="goToPage(currentPage - 1)"
|
||||
:disabled="currentPage <= 1"
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span class="sr-only">上一页</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<template v-for="page in Math.min(totalPages, 7)" :key="page">
|
||||
<button
|
||||
@click="goToPage(page)"
|
||||
:class="[
|
||||
page === currentPage
|
||||
? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
|
||||
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50',
|
||||
'relative inline-flex items-center px-4 py-2 border text-sm font-medium'
|
||||
]"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<button
|
||||
@click="goToPage(currentPage + 1)"
|
||||
:disabled="currentPage >= totalPages"
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span class="sr-only">下一页</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加用户模态框 -->
|
||||
<div v-if="showCreateModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">添加新用户</h3>
|
||||
<button @click="showCreateModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitCreateUser" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 基本信息 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱地址 *</label>
|
||||
<input
|
||||
v-model="newUser.email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入邮箱地址"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">密码 *</label>
|
||||
<input
|
||||
v-model="newUser.password"
|
||||
type="password"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">姓名 *</label>
|
||||
<input
|
||||
v-model="newUser.full_name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">手机号</label>
|
||||
<input
|
||||
v-model="newUser.phone"
|
||||
type="tel"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">角色 *</label>
|
||||
<select
|
||||
v-model="newUser.role"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">请选择角色</option>
|
||||
<option value="admin">管理员</option>
|
||||
<option value="customer">客户</option>
|
||||
<option value="interpreter">翻译员</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">初始余额</label>
|
||||
<input
|
||||
v-model.number="newUser.credits"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 翻译员专用字段 -->
|
||||
<div v-if="newUser.role === 'interpreter'" class="space-y-4 border-t pt-4">
|
||||
<h4 class="text-md font-medium text-gray-900">翻译员专业信息</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">公司</label>
|
||||
<input
|
||||
v-model="newUser.company"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="所属公司"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">部门</label>
|
||||
<input
|
||||
v-model="newUser.department"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="所属部门"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">时薪(元/小时)</label>
|
||||
<input
|
||||
v-model.number="newUser.hourly_rate"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="100.00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">时区</label>
|
||||
<select
|
||||
v-model="newUser.timezone"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="UTC+8">北京时间 (UTC+8)</option>
|
||||
<option value="UTC">协调世界时 (UTC)</option>
|
||||
<option value="UTC-5">美国东部时间 (UTC-5)</option>
|
||||
<option value="UTC-8">美国西部时间 (UTC-8)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">专业领域</label>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<label v-for="spec in specializationOptions" :key="spec" class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="spec"
|
||||
v-model="newUser.specializations"
|
||||
class="mr-2 text-blue-600"
|
||||
/>
|
||||
<span class="text-sm text-gray-700">{{ spec }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="showCreateModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
||||
>
|
||||
创建用户
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑用户模态框 -->
|
||||
<div v-if="showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">编辑用户</h3>
|
||||
<button @click="showEditModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitUpdateUser" class="space-y-4" v-if="editingUser">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱地址</label>
|
||||
<input
|
||||
v-model="editingUser.email"
|
||||
type="email"
|
||||
disabled
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">姓名 *</label>
|
||||
<input
|
||||
v-model="editingUser.full_name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">手机号</label>
|
||||
<input
|
||||
v-model="editingUser.phone"
|
||||
type="tel"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">角色 *</label>
|
||||
<select
|
||||
v-model="editingUser.role"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="admin">管理员</option>
|
||||
<option value="customer">客户</option>
|
||||
<option value="interpreter">翻译员</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">余额</label>
|
||||
<input
|
||||
v-model.number="editingUser.credits"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
|
||||
<select
|
||||
v-model="editingUser.status"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="active">活跃</option>
|
||||
<option value="inactive">非活跃</option>
|
||||
<option value="suspended">已暂停</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="showEditModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
||||
>
|
||||
更新用户
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 页面元数据 - 使用管理员认证和默认布局
|
||||
definePageMeta({
|
||||
middleware: 'admin-auth',
|
||||
layout: 'default' // 明确指定使用默认布局
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '用户管理 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 导入Supabase数据操作
|
||||
const { getProfiles } = useSupabaseData()
|
||||
|
||||
// 响应式数据
|
||||
const users = ref([])
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedRole = ref('')
|
||||
const selectedStatus = ref('')
|
||||
const currentPage = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const showCreateModal = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const allUsers = ref([]) // 存储所有用户数据用于筛选
|
||||
|
||||
// 用户统计数据
|
||||
const userStats = computed(() => {
|
||||
const total = allUsers.value.length
|
||||
const interpreters = allUsers.value.filter(user => user.role === 'interpreter').length
|
||||
const admins = allUsers.value.filter(user => user.role === 'admin').length
|
||||
const customers = allUsers.value.filter(user => user.role === 'customer').length
|
||||
|
||||
return {
|
||||
total,
|
||||
interpreters,
|
||||
admins,
|
||||
customers
|
||||
}
|
||||
})
|
||||
|
||||
// 计算属性:过滤后的用户列表
|
||||
const filteredUsers = computed(() => {
|
||||
let filtered = [...allUsers.value]
|
||||
|
||||
// 搜索筛选
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(user =>
|
||||
user.full_name?.toLowerCase().includes(query) ||
|
||||
user.email?.toLowerCase().includes(query) ||
|
||||
user.phone?.includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
// 角色筛选
|
||||
if (selectedRole.value) {
|
||||
filtered = filtered.filter(user => user.role === selectedRole.value)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (selectedStatus.value) {
|
||||
filtered = filtered.filter(user => user.status === selectedStatus.value)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// 计算属性:分页后的用户列表
|
||||
const paginatedUsers = computed(() => {
|
||||
const pageSize = 20
|
||||
totalPages.value = Math.ceil(filteredUsers.value.length / pageSize)
|
||||
const startIndex = (currentPage.value - 1) * pageSize
|
||||
const endIndex = startIndex + pageSize
|
||||
return filteredUsers.value.slice(startIndex, endIndex)
|
||||
})
|
||||
|
||||
// 新用户表单数据
|
||||
const newUser = ref({
|
||||
email: '',
|
||||
password: '',
|
||||
full_name: '',
|
||||
role: '',
|
||||
phone: '',
|
||||
company: '',
|
||||
department: '',
|
||||
specializations: [],
|
||||
hourly_rate: null,
|
||||
timezone: 'UTC'
|
||||
})
|
||||
|
||||
// 角色选项
|
||||
const roleOptions = [
|
||||
{ value: '', label: '所有角色' },
|
||||
{ value: 'admin', label: '管理员' },
|
||||
{ value: 'customer', label: '客户' },
|
||||
{ value: 'interpreter', label: '翻译员' }
|
||||
]
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ value: '', label: '所有状态' },
|
||||
{ value: 'active', label: '活跃' },
|
||||
{ value: 'inactive', label: '非活跃' },
|
||||
{ value: 'suspended', label: '已暂停' }
|
||||
]
|
||||
|
||||
// 专业领域选项
|
||||
const specializationOptions = [
|
||||
'医疗翻译', '法律翻译', '技术翻译', '商务翻译',
|
||||
'学术翻译', '金融翻译', '手语翻译', '会议翻译'
|
||||
]
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
console.log('开始获取用户数据...')
|
||||
|
||||
// 临时使用模拟数据,避免Supabase连接问题
|
||||
const mockUsers = [
|
||||
{
|
||||
id: '1',
|
||||
email: 'admin@example.com',
|
||||
full_name: '系统管理员',
|
||||
phone: '13800138000',
|
||||
role: 'admin',
|
||||
credits: 1000,
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString(),
|
||||
is_enterprise: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
email: 'translator1@example.com',
|
||||
full_name: '李译员',
|
||||
phone: '13800138001',
|
||||
role: 'interpreter',
|
||||
credits: 500,
|
||||
status: 'active',
|
||||
created_at: new Date(Date.now() - 86400000).toISOString(),
|
||||
is_enterprise: false
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
email: 'customer1@example.com',
|
||||
full_name: '张客户',
|
||||
phone: '13800138002',
|
||||
role: 'customer',
|
||||
credits: 200,
|
||||
status: 'active',
|
||||
created_at: new Date(Date.now() - 172800000).toISOString(),
|
||||
is_enterprise: false
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
email: 'translator2@example.com',
|
||||
full_name: '王译员',
|
||||
phone: '13800138003',
|
||||
role: 'interpreter',
|
||||
credits: 750,
|
||||
status: 'inactive',
|
||||
created_at: new Date(Date.now() - 259200000).toISOString(),
|
||||
is_enterprise: false
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
email: 'customer2@example.com',
|
||||
full_name: '陈客户',
|
||||
phone: '13800138004',
|
||||
role: 'customer',
|
||||
credits: 150,
|
||||
status: 'suspended',
|
||||
created_at: new Date(Date.now() - 345600000).toISOString(),
|
||||
is_enterprise: true
|
||||
}
|
||||
]
|
||||
|
||||
allUsers.value = mockUsers
|
||||
console.log('用户数据加载成功:', allUsers.value.length, '个用户')
|
||||
|
||||
// 正式版本应该使用:
|
||||
// const profilesData = await getProfiles()
|
||||
// allUsers.value = profilesData || []
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
allUsers.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置筛选条件
|
||||
const resetFilters = () => {
|
||||
searchQuery.value = ''
|
||||
selectedRole.value = ''
|
||||
selectedStatus.value = ''
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 筛选用户数据(保留原函数但不再需要)
|
||||
const filterUsers = () => {
|
||||
// 这个函数现在由计算属性 filteredUsers 和 paginatedUsers 处理
|
||||
// 保留空函数以防其他地方调用
|
||||
}
|
||||
|
||||
// 提交创建用户表单
|
||||
const submitCreateUser = async () => {
|
||||
try {
|
||||
console.log('创建新用户:', newUser.value)
|
||||
|
||||
// 这里应该调用Supabase的用户创建API
|
||||
// 暂时使用模拟数据添加到列表中
|
||||
const newUserData = {
|
||||
id: Date.now().toString(),
|
||||
...newUser.value,
|
||||
credits: newUser.value.credits || 0,
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString(),
|
||||
is_enterprise: false
|
||||
}
|
||||
|
||||
allUsers.value.push(newUserData)
|
||||
|
||||
showCreateModal.value = false
|
||||
resetNewUserForm()
|
||||
|
||||
alert('用户创建成功!')
|
||||
} catch (error) {
|
||||
console.error('创建用户失败:', error)
|
||||
alert('创建用户失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户按钮处理
|
||||
const createUser = () => {
|
||||
showCreateModal.value = true
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const editUser = (user) => {
|
||||
editingUser.value = { ...user }
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
// 提交更新用户表单
|
||||
const submitUpdateUser = async () => {
|
||||
try {
|
||||
console.log('更新用户:', editingUser.value)
|
||||
|
||||
// 这里应该调用Supabase的用户更新API
|
||||
// 暂时更新本地数据
|
||||
const index = allUsers.value.findIndex(u => u.id === editingUser.value.id)
|
||||
if (index !== -1) {
|
||||
allUsers.value[index] = { ...editingUser.value }
|
||||
}
|
||||
|
||||
showEditModal.value = false
|
||||
editingUser.value = null
|
||||
|
||||
alert('用户更新成功!')
|
||||
} catch (error) {
|
||||
console.error('更新用户失败:', error)
|
||||
alert('更新用户失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (userId) => {
|
||||
if (confirm('确定要删除此用户吗?此操作将禁用用户账户。')) {
|
||||
try {
|
||||
// 注意:这里需要使用Supabase的用户删除API
|
||||
// 暂时保留原API调用,后续可以改为直接使用Supabase
|
||||
await $fetch('/api/admin/users', {
|
||||
method: 'DELETE',
|
||||
body: { userId }
|
||||
})
|
||||
await fetchUsers()
|
||||
// 显示成功提示
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
// 显示错误提示
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置新用户表单
|
||||
const resetNewUserForm = () => {
|
||||
newUser.value = {
|
||||
email: '',
|
||||
password: '',
|
||||
full_name: '',
|
||||
role: '',
|
||||
phone: '',
|
||||
company: '',
|
||||
department: '',
|
||||
specializations: [],
|
||||
hourly_rate: null,
|
||||
timezone: 'UTC',
|
||||
credits: 0
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '从未登录'
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取状态显示文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'suspended': '已暂停',
|
||||
'inactive': '非活跃',
|
||||
'active': '活跃'
|
||||
}
|
||||
return statusMap[status] || '未知状态'
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status) => {
|
||||
const classMap = {
|
||||
'suspended': 'bg-red-100 text-red-800',
|
||||
'active': 'bg-green-100 text-green-800',
|
||||
'inactive': 'bg-yellow-100 text-yellow-800'
|
||||
}
|
||||
return classMap[status] || 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
// 获取角色显示文本
|
||||
const getRoleText = (role) => {
|
||||
const roleMap = {
|
||||
admin: '管理员',
|
||||
customer: '客户',
|
||||
interpreter: '翻译员'
|
||||
}
|
||||
return roleMap[role] || role
|
||||
}
|
||||
|
||||
// 监听搜索和筛选变化
|
||||
watch([searchQuery, selectedRole, selectedStatus], () => {
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
})
|
||||
|
||||
// 分页处理
|
||||
const goToPage = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 页面挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
})
|
||||
|
||||
// 导出用户数据
|
||||
const exportUsers = () => {
|
||||
alert('导出功能开发中...')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 页面头部 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">添加用户</h1>
|
||||
<p class="text-gray-600 mt-1">创建新的系统用户</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
@click="$router.back()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户表单 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<form @submit.prevent="handleSubmit" class="p-6 space-y-6">
|
||||
<!-- 基本信息 -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">基本信息</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
姓名 *
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="userForm.name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入用户姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
邮箱地址 *
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="userForm.email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入邮箱地址"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
手机号码 *
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
v-model="userForm.phone"
|
||||
type="tel"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="role" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
用户角色 *
|
||||
</label>
|
||||
<select
|
||||
id="role"
|
||||
v-model="userForm.role"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">请选择用户角色</option>
|
||||
<option value="user">普通用户</option>
|
||||
<option value="interpreter">译员</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="gender" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
性别
|
||||
</label>
|
||||
<select
|
||||
id="gender"
|
||||
v-model="userForm.gender"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">请选择性别</option>
|
||||
<option value="male">男</option>
|
||||
<option value="female">女</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="birthDate" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
出生日期
|
||||
</label>
|
||||
<input
|
||||
id="birthDate"
|
||||
v-model="userForm.birthDate"
|
||||
type="date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 账户设置 -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">账户设置</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
初始密码 *
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="userForm.password"
|
||||
type="password"
|
||||
required
|
||||
minlength="6"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入初始密码(至少6位)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
确认密码 *
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
v-model="userForm.confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
minlength="6"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
账户状态 *
|
||||
</label>
|
||||
<select
|
||||
id="status"
|
||||
v-model="userForm.status"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="active">激活</option>
|
||||
<option value="inactive">未激活</option>
|
||||
<option value="suspended">暂停</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="requirePasswordChange"
|
||||
v-model="userForm.requirePasswordChange"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="requirePasswordChange" class="ml-2 block text-sm text-gray-900">
|
||||
首次登录要求修改密码
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 译员专业信息(仅译员角色显示)-->
|
||||
<div v-if="userForm.role === 'interpreter'">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">译员专业信息</h3>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
专业语言 *
|
||||
</label>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div v-for="lang in availableLanguages" :key="lang.code" class="flex items-center">
|
||||
<input
|
||||
:id="`lang_${lang.code}`"
|
||||
v-model="userForm.languages"
|
||||
:value="lang.code"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label :for="`lang_${lang.code}`" class="ml-2 block text-sm text-gray-900">
|
||||
{{ lang.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="experience" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
工作经验(年)
|
||||
</label>
|
||||
<input
|
||||
id="experience"
|
||||
v-model.number="userForm.experience"
|
||||
type="number"
|
||||
min="0"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入工作经验"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="hourlyRate" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
时薪(元/小时)
|
||||
</label>
|
||||
<input
|
||||
id="hourlyRate"
|
||||
v-model.number="userForm.hourlyRate"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入时薪"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="specialties" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
专业领域
|
||||
</label>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div v-for="specialty in availableSpecialties" :key="specialty" class="flex items-center">
|
||||
<input
|
||||
:id="`specialty_${specialty}`"
|
||||
v-model="userForm.specialties"
|
||||
:value="specialty"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label :for="`specialty_${specialty}`" class="ml-2 block text-sm text-gray-900">
|
||||
{{ specialty }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="certifications" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
资质证书
|
||||
</label>
|
||||
<textarea
|
||||
id="certifications"
|
||||
v-model="userForm.certifications"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入相关资质证书信息"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 联系信息 -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">联系信息</h3>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="address" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
地址
|
||||
</label>
|
||||
<textarea
|
||||
id="address"
|
||||
v-model="userForm.address"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入详细地址"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="notes" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
备注
|
||||
</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
v-model="userForm.notes"
|
||||
rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入备注信息"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div class="flex justify-end space-x-3 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
@click="$router.back()"
|
||||
class="px-6 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isSubmitting"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ isSubmitting ? '创建中...' : '创建用户' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 页面元数据
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
// 页面标题
|
||||
useHead({
|
||||
title: '添加用户 - 翻译管理系统'
|
||||
})
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 表单数据
|
||||
const userForm = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
gender: '',
|
||||
birthDate: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
status: 'active',
|
||||
requirePasswordChange: false,
|
||||
languages: [],
|
||||
experience: 0,
|
||||
hourlyRate: 0,
|
||||
specialties: [],
|
||||
certifications: '',
|
||||
address: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
// 提交状态
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
// 可用语言列表
|
||||
const availableLanguages = [
|
||||
{ code: 'zh', name: '中文' },
|
||||
{ code: 'en', name: '英文' },
|
||||
{ code: 'ja', name: '日文' },
|
||||
{ code: 'ko', name: '韩文' },
|
||||
{ code: 'fr', name: '法文' },
|
||||
{ code: 'de', name: '德文' },
|
||||
{ code: 'es', name: '西班牙文' },
|
||||
{ code: 'ru', name: '俄文' },
|
||||
{ code: 'ar', name: '阿拉伯文' },
|
||||
{ code: 'it', name: '意大利文' }
|
||||
]
|
||||
|
||||
// 专业领域列表
|
||||
const availableSpecialties = [
|
||||
'商务会议',
|
||||
'法律翻译',
|
||||
'医疗翻译',
|
||||
'技术翻译',
|
||||
'学术会议',
|
||||
'旅游陪同',
|
||||
'展会翻译',
|
||||
'政府会议',
|
||||
'金融翻译',
|
||||
'文学翻译'
|
||||
]
|
||||
|
||||
// 处理表单提交
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
isSubmitting.value = true
|
||||
|
||||
// 基本验证
|
||||
if (!userForm.value.name || !userForm.value.email || !userForm.value.phone ||
|
||||
!userForm.value.role || !userForm.value.password || !userForm.value.confirmPassword) {
|
||||
throw new Error('请填写所有必填字段')
|
||||
}
|
||||
|
||||
// 密码确认验证
|
||||
if (userForm.value.password !== userForm.value.confirmPassword) {
|
||||
throw new Error('两次输入的密码不一致')
|
||||
}
|
||||
|
||||
// 译员角色语言验证
|
||||
if (userForm.value.role === 'interpreter' && userForm.value.languages.length === 0) {
|
||||
throw new Error('译员角色需要选择至少一种专业语言')
|
||||
}
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 创建用户数据
|
||||
const newUser = {
|
||||
id: `usr_${Date.now()}`,
|
||||
...userForm.value,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
// 移除确认密码字段
|
||||
delete newUser.confirmPassword
|
||||
|
||||
console.log('创建用户:', newUser)
|
||||
|
||||
// 显示成功消息
|
||||
alert('用户创建成功')
|
||||
|
||||
// 返回用户列表页面
|
||||
router.push('/users')
|
||||
|
||||
} catch (error) {
|
||||
alert(error.message || '创建用户失败,请重试')
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user