first commit

This commit is contained in:
Mars Developer
2025-06-26 11:24:11 +08:00
commit 51f8d95bf9
46 changed files with 20691 additions and 0 deletions
+289
View File
@@ -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>
+102
View File
@@ -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>
+478
View File
@@ -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>
+45
View File
@@ -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
View File
@@ -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>
+924
View File
@@ -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>
+415
View File
@@ -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>
+445
View File
@@ -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>
+339
View File
@@ -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
View File
@@ -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>
+439
View File
@@ -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>