增加管理员页面

This commit is contained in:
2026-04-28 21:01:56 +08:00
parent 7db64658f9
commit b0c272d740
5 changed files with 265 additions and 128 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ops_vue_js", "name": "ops_vue_js",
"version": "1.0.1", "version": "1.2.1",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
@@ -158,7 +158,7 @@ const navItems = computed(() => [
@click="userDropdownOpen = false" @click="userDropdownOpen = false"
> >
<IconShield :size="16" /> <IconShield :size="16" />
系统管理 {{ t("message.sysadmin") }}
</RouterLink> </RouterLink>
<hr class="my-1 border-gray-200 dark:border-dk-muted" /> <hr class="my-1 border-gray-200 dark:border-dk-muted" />
<button <button
@@ -233,7 +233,7 @@ const navItems = computed(() => [
@click="userDropdownOpen = false" @click="userDropdownOpen = false"
> >
<IconShield :size="16" /> <IconShield :size="16" />
系统管理 {{ t("message.sysadmin") }}
</RouterLink> </RouterLink>
<hr class="my-1 border-gray-200 dark:border-dk-muted" /> <hr class="my-1 border-gray-200 dark:border-dk-muted" />
<button <button
+69
View File
@@ -353,6 +353,7 @@
"user_settings": "Settings", "user_settings": "Settings",
"preferences": "Preferences", "preferences": "Preferences",
"administrator": "Administrator", "administrator": "Administrator",
"sysadmin": "System Admin",
"select_date": "Select a date", "select_date": "Select a date",
"save_ok": "Saved successfully", "save_ok": "Saved successfully",
"delete_ok": "Deleted successfully", "delete_ok": "Deleted successfully",
@@ -435,5 +436,73 @@
"returning": "Returning", "returning": "Returning",
"refunded": "Refunded", "refunded": "Refunded",
"lost_package": "Lost Package" "lost_package": "Lost Package"
},
"sysadmin": {
"title": "System Admin",
"subtitle": "Admin Only Page",
"admin_label": "Admin",
"tab_users": "User Management",
"tab_groups": "User Groups",
"tab_logs": "Login Logs",
"total_users": "Total {count} users",
"search_placeholder": "Search username or email...",
"search": "Search",
"refresh": "Refresh",
"loading": "Loading...",
"no_data": "No data",
"no_users": "No users",
"no_groups": "No groups",
"no_members": "No members",
"no_logs": "No login failure logs",
"table_id": "ID",
"table_username": "Username",
"table_email": "Email",
"table_type": "Type",
"table_created_at": "Created At",
"table_action": "Action",
"table_user": "User",
"table_reason": "Reason",
"table_count": "Count",
"table_ip": "IP Address",
"table_updated_at": "Last Time",
"table_created": "First Time",
"reason_password_error": "Wrong Password",
"reason_user_not_found": "User Not Found",
"detail": "Detail",
"add_member": "Add Member",
"remove_member": "Remove",
"remove_member_title": "Remove Member",
"remove_member_confirm": "Are you sure to remove this member?",
"add_member_title": "Add Member to {name}",
"search_user_placeholder": "Search username or email...",
"searching": "Searching...",
"no_search_results": "No matching users found",
"search_hint": "Enter keywords to search users",
"add": "Add",
"select_group_hint": "Please select a group to view members",
"total_members": "Total {count} members",
"pagination": "Page {current} of {total}",
"prev_page": "Previous",
"next_page": "Next",
"user_detail": "User Detail",
"close": "Close",
"user_id": "User ID",
"name": "Username",
"birthday": "Birthday",
"gender": "Gender",
"region": "Region",
"language": "Language",
"info_nickname": "Nickname",
"info_remark": "Remark",
"reset_password": "Reset Password",
"new_password_placeholder": "Enter new password (min 6 chars)",
"reset_password_btn": "Reset Password",
"resetting": "Resetting...",
"search_log_placeholder": "Search username or IP address...",
"total_logs": "Total {count} records",
"current_admins": "Current System Admins",
"no_admins": "No system admins",
"group_list": "Group List",
"extended_info": "Extended Info"
} }
} }
+69
View File
@@ -353,6 +353,7 @@
"user_settings": "个人资料", "user_settings": "个人资料",
"preferences": "偏好设置", "preferences": "偏好设置",
"administrator": "管理员", "administrator": "管理员",
"sysadmin": "系统管理",
"select_date": "选择日期", "select_date": "选择日期",
"save_ok": "保存成功", "save_ok": "保存成功",
"change_ok": "更改成功", "change_ok": "更改成功",
@@ -435,5 +436,73 @@
"returning": "退回中", "returning": "退回中",
"refunded": "已退款", "refunded": "已退款",
"lost_package": "丢件" "lost_package": "丢件"
},
"sysadmin": {
"title": "系统管理",
"subtitle": "系统管理员专用页面",
"admin_label": "管理员",
"tab_users": "用户管理",
"tab_groups": "用户组",
"tab_logs": "登录日志",
"total_users": "共 {count} 位用户",
"search_placeholder": "搜索用户名或邮箱...",
"search": "搜索",
"refresh": "刷新",
"loading": "加载中...",
"no_data": "暂无数据",
"no_users": "暂无用户",
"no_groups": "暂无用户组",
"no_members": "暂无成员",
"no_logs": "暂无登录失败记录",
"table_id": "ID",
"table_username": "用户名",
"table_email": "邮箱",
"table_type": "类型",
"table_created_at": "注册时间",
"table_action": "操作",
"table_user": "用户",
"table_reason": "失败原因",
"table_count": "连续次数",
"table_ip": "IP地址",
"table_updated_at": "最后时间",
"table_created": "首次时间",
"reason_password_error": "密码错误",
"reason_user_not_found": "用户不存在",
"detail": "详情",
"add_member": "添加成员",
"remove_member": "移除",
"remove_member_title": "移除成员",
"remove_member_confirm": "确定要移除该成员吗?",
"add_member_title": "添加成员到 {name}",
"search_user_placeholder": "搜索用户名或邮箱...",
"searching": "搜索中...",
"no_search_results": "未找到匹配的用户",
"search_hint": "输入关键词搜索用户",
"add": "添加",
"select_group_hint": "请选择一个用户组查看成员",
"total_members": "共 {count} 位成员",
"pagination": "第 {current} 页,共 {total} 页",
"prev_page": "上一页",
"next_page": "下一页",
"user_detail": "用户详情",
"close": "关闭",
"user_id": "用户ID",
"name": "用户名",
"birthday": "生日",
"gender": "性别",
"region": "地区",
"language": "语言",
"info_nickname": "昵称",
"info_remark": "备注",
"reset_password": "重置密码",
"new_password_placeholder": "输入新密码(至少6位)",
"reset_password_btn": "修改密码",
"resetting": "修改中...",
"search_log_placeholder": "搜索用户名或IP地址...",
"total_logs": "共 {count} 条记录",
"current_admins": "当前系统管理员列表",
"no_admins": "暂无系统管理员",
"group_list": "用户组列表",
"extended_info": "扩展信息"
} }
} }
+99 -100
View File
@@ -1,5 +1,6 @@
<script setup> <script setup>
import { ref, onMounted, watch, computed } from 'vue' import { ref, onMounted, watch, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useUsersStore } from '@/stores/users' import { useUsersStore } from '@/stores/users'
import { useToastStore } from '@/stores/toast' import { useToastStore } from '@/stores/toast'
@@ -7,6 +8,8 @@ import { authApi } from '@/api/auth'
import ConfirmDialog from '@/components/ConfirmDialog.vue' import ConfirmDialog from '@/components/ConfirmDialog.vue'
import { IconSearch, IconRefresh, IconChevronLeft, IconChevronRight, IconPlus, IconX } from '@tabler/icons-vue' import { IconSearch, IconRefresh, IconChevronLeft, IconChevronRight, IconPlus, IconX } from '@tabler/icons-vue'
const { t } = useI18n()
const toast = useToastStore() const toast = useToastStore()
const usersStore = useUsersStore() const usersStore = useUsersStore()
@@ -62,9 +65,9 @@ const confirmDialogConfig = ref({
}) })
const tabs = [ const tabs = [
{ id: 'users', label: '用户管理' }, { id: 'users', label: t('sysadmin.tab_users') },
{ id: 'groups', label: '用户组' }, { id: 'groups', label: t('sysadmin.tab_groups') },
{ id: 'logs', label: '登录日志' }, { id: 'logs', label: t('sysadmin.tab_logs') },
] ]
async function fetchSysAdmins() { async function fetchSysAdmins() {
@@ -221,12 +224,12 @@ async function addGroupMember(userId) {
try { try {
const res = await authApi.addGroupMember(selectedGroup.value.id, userId) const res = await authApi.addGroupMember(selectedGroup.value.id, userId)
if (res.errCode === 0) { if (res.errCode === 0) {
toast.success('成员添加成功') toast.success(t('sysadmin.add_member') + t('message.save_ok'))
fetchGroupMembers() fetchGroupMembers()
// 从搜索结果中移除已添加的用户 // 从搜索结果中移除已添加的用户
addMemberSearchResults.value = addMemberSearchResults.value.filter(u => u.id !== userId) addMemberSearchResults.value = addMemberSearchResults.value.filter(u => u.id !== userId)
} else { } else {
toast.error(res.raw?.err_msg || '添加失败') toast.error(res.raw?.err_msg || t('sysadmin.add_member') + t('message.save_ok'))
} }
} catch { } catch {
// 错误已由拦截器处理 // 错误已由拦截器处理
@@ -250,18 +253,18 @@ function handleConfirm() {
async function removeGroupMember(userId) { async function removeGroupMember(userId) {
if (!selectedGroup.value) return if (!selectedGroup.value) return
openConfirmDialog({ openConfirmDialog({
title: '移除成员', title: t('sysadmin.remove_member_title'),
message: '确定要移除该成员吗?', message: t('sysadmin.remove_member_confirm'),
confirmText: '移除', confirmText: t('sysadmin.remove_member'),
danger: true, danger: true,
onConfirm: async () => { onConfirm: async () => {
try { try {
const res = await authApi.removeGroupMember(selectedGroup.value.id, userId) const res = await authApi.removeGroupMember(selectedGroup.value.id, userId)
if (res.errCode === 0) { if (res.errCode === 0) {
toast.success('成员移除成功') toast.success(t('sysadmin.remove_member_title') + t('message.delete_ok'))
fetchGroupMembers() fetchGroupMembers()
} else { } else {
toast.error(res.raw?.err_msg || '移除失败') toast.error(res.raw?.err_msg || t('sysadmin.remove_member') + t('message.delete_ok'))
} }
} catch { } catch {
// 错误已由拦截器处理 // 错误已由拦截器处理
@@ -309,8 +312,8 @@ function onLoginFailLogPageChange(page) {
function formatReason(reason) { function formatReason(reason) {
const reasonMap = { const reasonMap = {
'password_error': '密码错误', 'password_error': t('sysadmin.reason_password_error'),
'user_not_found': '用户不存在', 'user_not_found': t('sysadmin.reason_user_not_found'),
} }
return reasonMap[reason] || reason return reasonMap[reason] || reason
} }
@@ -352,7 +355,7 @@ function closeUserDetail() {
async function resetUserPassword() { async function resetUserPassword() {
if (!newPassword.value || newPassword.value.length < 6) { if (!newPassword.value || newPassword.value.length < 6) {
toast.warning('密码长度至少为6位') toast.warning(t('sysadmin.new_password_placeholder'))
return return
} }
if (!userDetail.value) return if (!userDetail.value) return
@@ -361,10 +364,10 @@ async function resetUserPassword() {
try { try {
const res = await authApi.resetUserPassword(userDetail.value.id, newPassword.value) const res = await authApi.resetUserPassword(userDetail.value.id, newPassword.value)
if (res.errCode === 0) { if (res.errCode === 0) {
toast.success('密码修改成功') toast.success(t('message.change_ok'))
newPassword.value = '' newPassword.value = ''
} else { } else {
toast.error(res.raw?.err_msg || '密码修改失败') toast.error(res.raw?.err_msg || t('message.change_ok'))
} }
} catch { } catch {
// 错误已由拦截器处理 // 错误已由拦截器处理
@@ -379,8 +382,8 @@ function formatDate(dateStr) {
} }
function formatGender(gender) { function formatGender(gender) {
const map = { 'M': '男', 'F': '女', 'U': '未知' } const map = { 'M': t('settings.male'), 'F': t('settings.female'), 'U': '-' }
return map[gender] || '未知' return map[gender] || '-'
} }
// 监听 Tab 切换 // 监听 Tab 切换
@@ -409,13 +412,13 @@ onMounted(() => {
<!-- Header --> <!-- Header -->
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<div> <div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-dk-text">系统管理</h1> <h1 class="text-2xl font-bold text-gray-900 dark:text-dk-text">{{ t('sysadmin.title') }}</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-dk-subtle"> <p class="mt-1 text-sm text-gray-500 dark:text-dk-subtle">
系统管理员专用页面 {{ t('sysadmin.subtitle') }}
</p> </p>
</div> </div>
<div class="flex items-center gap-2 rounded-lg bg-amber-100 px-3 py-1.5 dark:bg-amber-900/30"> <div class="flex items-center gap-2 rounded-lg bg-amber-100 px-3 py-1.5 dark:bg-amber-900/30">
<span class="text-amber-700 dark:text-amber-400">管理员: {{ userStore.user?.Username }}</span> <span class="text-amber-700 dark:text-amber-400">{{ t('sysadmin.admin_label') }}: {{ userStore.user?.Username }}</span>
</div> </div>
</div> </div>
@@ -443,9 +446,9 @@ onMounted(() => {
<!-- 用户管理 --> <!-- 用户管理 -->
<div v-if="activeTab === 'users'" class="space-y-4"> <div v-if="activeTab === 'users'" class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">用户管理</h2> <h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">{{ t('sysadmin.tab_users') }}</h2>
<span class="text-sm text-gray-500 dark:text-dk-subtle"> <span class="text-sm text-gray-500 dark:text-dk-subtle">
{{ userTotal }} 位用户 {{ t('sysadmin.total_users', { count: userTotal }) }}
</span> </span>
</div> </div>
@@ -456,7 +459,7 @@ onMounted(() => {
<input <input
v-model="userSearch" v-model="userSearch"
type="text" type="text"
placeholder="搜索用户名或邮箱..." :placeholder="t('sysadmin.search_placeholder')"
class="w-full rounded-md border border-gray-300 py-2 pl-9 pr-4 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text" class="w-full rounded-md border border-gray-300 py-2 pl-9 pr-4 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text"
@keyup.enter="onSearch" @keyup.enter="onSearch"
/> />
@@ -465,7 +468,7 @@ onMounted(() => {
@click="onSearch" @click="onSearch"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
> >
搜索 {{ t('sysadmin.search') }}
</button> </button>
<button <button
@click="fetchUsers" @click="fetchUsers"
@@ -481,20 +484,20 @@ onMounted(() => {
<table class="min-w-full divide-y divide-gray-200 dark:divide-dk-muted"> <table class="min-w-full divide-y divide-gray-200 dark:divide-dk-muted">
<thead class="bg-gray-50 dark:bg-dk-base"> <thead class="bg-gray-50 dark:bg-dk-base">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">ID</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_id') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">用户名</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_username') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">邮箱</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_email') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">类型</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_type') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">注册时间</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_created_at') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">操作</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_action') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-dk-muted dark:bg-dk-card"> <tbody class="divide-y divide-gray-200 bg-white dark:divide-dk-muted dark:bg-dk-card">
<tr v-if="usersLoading" class="text-center"> <tr v-if="usersLoading" class="text-center">
<td colspan="6" class="py-8 text-gray-500 dark:text-dk-subtle">加载中...</td> <td colspan="6" class="py-8 text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.loading') }}</td>
</tr> </tr>
<tr v-else-if="users.length === 0" class="text-center"> <tr v-else-if="users.length === 0" class="text-center">
<td colspan="6" class="py-8 text-gray-500 dark:text-dk-subtle">暂无用户</td> <td colspan="6" class="py-8 text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.no_users') }}</td>
</tr> </tr>
<tr v-for="user in users" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-dk-base"> <tr v-for="user in users" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-dk-base">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-dk-text">{{ user.id }}</td> <td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-dk-text">{{ user.id }}</td>
@@ -523,7 +526,7 @@ onMounted(() => {
</td> </td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-dk-subtle">{{ new Date(user.date).toLocaleString() }}</td> <td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-dk-subtle">{{ new Date(user.date).toLocaleString() }}</td>
<td class="whitespace-nowrap px-4 py-3 text-sm"> <td class="whitespace-nowrap px-4 py-3 text-sm">
<button @click="openUserDetail(user)" class="text-blue-600 hover:text-blue-700 dark:text-blue-400">详情</button> <button @click="openUserDetail(user)" class="text-blue-600 hover:text-blue-700 dark:text-blue-400">{{ t('sysadmin.detail') }}</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -533,7 +536,7 @@ onMounted(() => {
<!-- 分页 --> <!-- 分页 -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="text-sm text-gray-500 dark:text-dk-subtle"> <div class="text-sm text-gray-500 dark:text-dk-subtle">
{{ userPage }} {{ totalPages }} {{ t('sysadmin.pagination', { current: userPage, total: totalPages }) }}
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -541,14 +544,14 @@ onMounted(() => {
:disabled="userPage <= 1 || usersLoading" :disabled="userPage <= 1 || usersLoading"
class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text" class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text"
> >
<IconChevronLeft :size="16" /> 上一页 <IconChevronLeft :size="16" /> {{ t('sysadmin.prev_page') }}
</button> </button>
<button <button
@click="onPageChange(userPage + 1)" @click="onPageChange(userPage + 1)"
:disabled="userPage >= totalPages || usersLoading" :disabled="userPage >= totalPages || usersLoading"
class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text" class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text"
> >
下一页 <IconChevronRight :size="16" /> {{ t('sysadmin.next_page') }} <IconChevronRight :size="16" />
</button> </button>
</div> </div>
</div> </div>
@@ -557,7 +560,7 @@ onMounted(() => {
<!-- 用户组 --> <!-- 用户组 -->
<div v-if="activeTab === 'groups'" class="space-y-4"> <div v-if="activeTab === 'groups'" class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">用户组管理</h2> <h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">{{ t('sysadmin.tab_groups') }}</h2>
<button <button
@click="fetchGroups" @click="fetchGroups"
class="rounded-md border border-gray-300 px-3 py-1.5 text-gray-600 hover:bg-gray-50 dark:border-dk-muted dark:text-dk-subtle dark:hover:bg-dk-card" class="rounded-md border border-gray-300 px-3 py-1.5 text-gray-600 hover:bg-gray-50 dark:border-dk-muted dark:text-dk-subtle dark:hover:bg-dk-card"
@@ -572,13 +575,13 @@ onMounted(() => {
<div class="lg:col-span-1"> <div class="lg:col-span-1">
<div class="overflow-hidden rounded-md border border-gray-200 dark:border-dk-muted"> <div class="overflow-hidden rounded-md border border-gray-200 dark:border-dk-muted">
<div class="bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700 dark:bg-dk-base dark:text-dk-subtle"> <div class="bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700 dark:bg-dk-base dark:text-dk-subtle">
用户组列表 {{ t('sysadmin.group_list') }}
</div> </div>
<div v-if="groupsLoading" class="p-4 text-center text-gray-500 dark:text-dk-subtle"> <div v-if="groupsLoading" class="p-4 text-center text-gray-500 dark:text-dk-subtle">
加载中... {{ t('sysadmin.loading') }}
</div> </div>
<div v-else-if="groups.length === 0" class="p-4 text-center text-gray-500 dark:text-dk-subtle"> <div v-else-if="groups.length === 0" class="p-4 text-center text-gray-500 dark:text-dk-subtle">
暂无用户组 {{ t('sysadmin.no_groups') }}
</div> </div>
<div v-else class="divide-y divide-gray-200 dark:divide-dk-muted"> <div v-else class="divide-y divide-gray-200 dark:divide-dk-muted">
<button <button
@@ -605,19 +608,19 @@ onMounted(() => {
<!-- 组成员详情 --> <!-- 组成员详情 -->
<div class="lg:col-span-2"> <div class="lg:col-span-2">
<div v-if="!selectedGroup" class="rounded-md bg-gray-50 p-8 text-center dark:bg-dk-base"> <div v-if="!selectedGroup" class="rounded-md bg-gray-50 p-8 text-center dark:bg-dk-base">
<p class="text-gray-500 dark:text-dk-subtle">请选择一个用户组查看成员</p> <p class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.select_group_hint') }}</p>
</div> </div>
<div v-else class="space-y-4"> <div v-else class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h3 class="font-semibold text-gray-900 dark:text-dk-text">{{ selectedGroup.name }}</h3> <h3 class="font-semibold text-gray-900 dark:text-dk-text">{{ selectedGroup.name }}</h3>
<p class="text-sm text-gray-500 dark:text-dk-subtle"> {{ groupMemberTotal }} 位成员</p> <p class="text-sm text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.total_members', { count: groupMemberTotal }) }}</p>
</div> </div>
<button <button
@click="openAddMemberDialog" @click="openAddMemberDialog"
class="flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700" class="flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
> >
<IconPlus :size="16" /> 添加成员 <IconPlus :size="16" /> {{ t('sysadmin.add_member') }}
</button> </button>
</div> </div>
@@ -625,18 +628,18 @@ onMounted(() => {
<table class="min-w-full divide-y divide-gray-200 dark:divide-dk-muted"> <table class="min-w-full divide-y divide-gray-200 dark:divide-dk-muted">
<thead class="bg-gray-50 dark:bg-dk-base"> <thead class="bg-gray-50 dark:bg-dk-base">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">用户</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_user') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">邮箱</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_email') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">类型</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_type') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">操作</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_action') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-dk-muted dark:bg-dk-card"> <tbody class="divide-y divide-gray-200 bg-white dark:divide-dk-muted dark:bg-dk-card">
<tr v-if="groupMembersLoading" class="text-center"> <tr v-if="groupMembersLoading" class="text-center">
<td colspan="4" class="py-8 text-gray-500 dark:text-dk-subtle">加载中...</td> <td colspan="4" class="py-8 text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.loading') }}</td>
</tr> </tr>
<tr v-else-if="groupMembers.length === 0" class="text-center"> <tr v-else-if="groupMembers.length === 0" class="text-center">
<td colspan="4" class="py-8 text-gray-500 dark:text-dk-subtle">暂无成员</td> <td colspan="4" class="py-8 text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.no_members') }}</td>
</tr> </tr>
<tr v-for="member in groupMembers" :key="member.id" class="hover:bg-gray-50 dark:hover:bg-dk-base"> <tr v-for="member in groupMembers" :key="member.id" class="hover:bg-gray-50 dark:hover:bg-dk-base">
<td class="whitespace-nowrap px-4 py-3"> <td class="whitespace-nowrap px-4 py-3">
@@ -667,7 +670,7 @@ onMounted(() => {
@click="removeGroupMember(member.id)" @click="removeGroupMember(member.id)"
class="text-red-600 hover:text-red-700 dark:text-red-400" class="text-red-600 hover:text-red-700 dark:text-red-400"
> >
移除 {{ t('sysadmin.remove_member') }}
</button> </button>
</td> </td>
</tr> </tr>
@@ -678,7 +681,7 @@ onMounted(() => {
<!-- 分页 --> <!-- 分页 -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="text-sm text-gray-500 dark:text-dk-subtle"> <div class="text-sm text-gray-500 dark:text-dk-subtle">
{{ groupMemberPage }} {{ groupMemberTotalPages }} {{ t('sysadmin.pagination', { current: groupMemberPage, total: groupMemberTotalPages }) }}
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -686,14 +689,14 @@ onMounted(() => {
:disabled="groupMemberPage <= 1 || groupMembersLoading" :disabled="groupMemberPage <= 1 || groupMembersLoading"
class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text" class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text"
> >
<IconChevronLeft :size="16" /> 上一页 <IconChevronLeft :size="16" /> {{ t('sysadmin.prev_page') }}
</button> </button>
<button <button
@click="onGroupMemberPageChange(groupMemberPage + 1)" @click="onGroupMemberPageChange(groupMemberPage + 1)"
:disabled="groupMemberPage >= groupMemberTotalPages || groupMembersLoading" :disabled="groupMemberPage >= groupMemberTotalPages || groupMembersLoading"
class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text" class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text"
> >
下一页 <IconChevronRight :size="16" /> {{ t('sysadmin.next_page') }} <IconChevronRight :size="16" />
</button> </button>
</div> </div>
</div> </div>
@@ -705,9 +708,9 @@ onMounted(() => {
<!-- 登录日志 --> <!-- 登录日志 -->
<div v-if="activeTab === 'logs'" class="space-y-4"> <div v-if="activeTab === 'logs'" class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">登录失败日志</h2> <h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">{{ t('sysadmin.tab_logs') }}</h2>
<span class="text-sm text-gray-500 dark:text-dk-subtle"> <span class="text-sm text-gray-500 dark:text-dk-subtle">
{{ loginFailLogTotal }} 条记录 {{ t('sysadmin.total_logs', { count: loginFailLogTotal }) }}
</span> </span>
</div> </div>
@@ -718,7 +721,7 @@ onMounted(() => {
<input <input
v-model="loginFailLogSearch" v-model="loginFailLogSearch"
type="text" type="text"
placeholder="搜索用户名或IP地址..." :placeholder="t('sysadmin.search_log_placeholder')"
class="w-full rounded-md border border-gray-300 py-2 pl-9 pr-4 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text" class="w-full rounded-md border border-gray-300 py-2 pl-9 pr-4 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text"
@keyup.enter="onLoginFailLogSearch" @keyup.enter="onLoginFailLogSearch"
/> />
@@ -727,7 +730,7 @@ onMounted(() => {
@click="onLoginFailLogSearch" @click="onLoginFailLogSearch"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
> >
搜索 {{ t('sysadmin.search') }}
</button> </button>
<button <button
@click="fetchLoginFailLogs" @click="fetchLoginFailLogs"
@@ -743,20 +746,20 @@ onMounted(() => {
<table class="min-w-full divide-y divide-gray-200 dark:divide-dk-muted"> <table class="min-w-full divide-y divide-gray-200 dark:divide-dk-muted">
<thead class="bg-gray-50 dark:bg-dk-base"> <thead class="bg-gray-50 dark:bg-dk-base">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">用户名</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_user') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">失败原因</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_reason') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">连续次数</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_count') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">IP地址</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_ip') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">最后时间</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_updated_at') }}</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">首次时间</th> <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_created') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-dk-muted dark:bg-dk-card"> <tbody class="divide-y divide-gray-200 bg-white dark:divide-dk-muted dark:bg-dk-card">
<tr v-if="loginFailLogsLoading" class="text-center"> <tr v-if="loginFailLogsLoading" class="text-center">
<td colspan="6" class="py-8 text-gray-500 dark:text-dk-subtle">加载中...</td> <td colspan="6" class="py-8 text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.loading') }}</td>
</tr> </tr>
<tr v-else-if="loginFailLogs.length === 0" class="text-center"> <tr v-else-if="loginFailLogs.length === 0" class="text-center">
<td colspan="6" class="py-8 text-gray-500 dark:text-dk-subtle">暂无登录失败记录</td> <td colspan="6" class="py-8 text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.no_logs') }}</td>
</tr> </tr>
<tr v-for="log in loginFailLogs" :key="log.id" class="hover:bg-gray-50 dark:hover:bg-dk-base"> <tr v-for="log in loginFailLogs" :key="log.id" class="hover:bg-gray-50 dark:hover:bg-dk-base">
<td class="whitespace-nowrap px-4 py-3"> <td class="whitespace-nowrap px-4 py-3">
@@ -802,7 +805,7 @@ onMounted(() => {
<!-- 分页 --> <!-- 分页 -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="text-sm text-gray-500 dark:text-dk-subtle"> <div class="text-sm text-gray-500 dark:text-dk-subtle">
{{ loginFailLogPage }} {{ loginFailLogTotalPages }} {{ t('sysadmin.pagination', { current: loginFailLogPage, total: loginFailLogTotalPages }) }}
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -810,14 +813,14 @@ onMounted(() => {
:disabled="loginFailLogPage <= 1 || loginFailLogsLoading" :disabled="loginFailLogPage <= 1 || loginFailLogsLoading"
class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text" class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text"
> >
<IconChevronLeft :size="16" /> 上一页 <IconChevronLeft :size="16" /> {{ t('sysadmin.prev_page') }}
</button> </button>
<button <button
@click="onLoginFailLogPageChange(loginFailLogPage + 1)" @click="onLoginFailLogPageChange(loginFailLogPage + 1)"
:disabled="loginFailLogPage >= loginFailLogTotalPages || loginFailLogsLoading" :disabled="loginFailLogPage >= loginFailLogTotalPages || loginFailLogsLoading"
class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text" class="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50 dark:border-dk-muted dark:text-dk-text"
> >
下一页 <IconChevronRight :size="16" /> {{ t('sysadmin.next_page') }} <IconChevronRight :size="16" />
</button> </button>
</div> </div>
</div> </div>
@@ -828,13 +831,13 @@ onMounted(() => {
<!-- SysAdmins List --> <!-- SysAdmins List -->
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4 dark:border-dk-muted dark:bg-dk-card"> <div class="mt-6 rounded-lg border border-gray-200 bg-white p-4 dark:border-dk-muted dark:bg-dk-card">
<div class="mb-2 flex items-center justify-between"> <div class="mb-2 flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700 dark:text-dk-subtle">当前系统管理员列表</h3> <h3 class="text-sm font-medium text-gray-700 dark:text-dk-subtle">{{ t('sysadmin.current_admins') }}</h3>
<button <button
@click="fetchSysAdmins" @click="fetchSysAdmins"
class="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400" class="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400"
:disabled="loading" :disabled="loading"
> >
{{ loading ? '加载中...' : '刷新' }} {{ loading ? t('sysadmin.loading') : t('sysadmin.refresh') }}
</button> </button>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@@ -843,17 +846,13 @@ onMounted(() => {
:key="adminId" :key="adminId"
class="flex items-center gap-2 rounded-full bg-amber-100 px-3 py-1 dark:bg-amber-900/30" class="flex items-center gap-2 rounded-full bg-amber-100 px-3 py-1 dark:bg-amber-900/30"
> >
<img <img :src="usersStore.getAvatarUrlFromUserID(adminId)" class="w-5 h-5 rounded-full" alt="avatar" />
:src="usersStore.getAvatarUrlFromUserID(adminId)"
class="h-6 w-6 rounded-full object-cover"
alt="avatar"
/>
<span class="text-xs font-medium text-amber-800 dark:text-amber-400"> <span class="text-xs font-medium text-amber-800 dark:text-amber-400">
{{ usersStore.getUsernameFromUserID(adminId) || 'ID: ' + adminId }} {{ usersStore.getUsernameFromUserID(adminId) || 'ID: ' + adminId }}
</span> </span>
</div> </div>
<span v-if="sysAdmins.length === 0" class="text-sm text-gray-400 dark:text-dk-muted"> <span v-if="sysAdmins.length === 0" class="text-sm text-gray-400 dark:text-dk-muted">
暂无系统管理员 {{ t('sysadmin.no_admins') }}
</span> </span>
</div> </div>
</div> </div>
@@ -868,7 +867,7 @@ onMounted(() => {
> >
<div class="w-full max-w-lg rounded-lg bg-white p-6 shadow-xl dark:bg-dk-card"> <div class="w-full max-w-lg rounded-lg bg-white p-6 shadow-xl dark:bg-dk-card">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-dk-text">用户详情</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-dk-text">{{ t('sysadmin.user_detail') }}</h3>
<button <button
@click="closeUserDetail" @click="closeUserDetail"
class="text-gray-400 hover:text-gray-600 dark:text-dk-subtle dark:hover:text-dk-text" class="text-gray-400 hover:text-gray-600 dark:text-dk-subtle dark:hover:text-dk-text"
@@ -880,7 +879,7 @@ onMounted(() => {
</div> </div>
<div v-if="userDetailLoading" class="py-8 text-center text-gray-500 dark:text-dk-subtle"> <div v-if="userDetailLoading" class="py-8 text-center text-gray-500 dark:text-dk-subtle">
加载中... {{ t('sysadmin.loading') }}
</div> </div>
<div v-else-if="userDetail" class="space-y-4"> <div v-else-if="userDetail" class="space-y-4">
@@ -912,44 +911,44 @@ onMounted(() => {
<!-- 详细信息 --> <!-- 详细信息 -->
<div class="space-y-2 text-sm"> <div class="space-y-2 text-sm">
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">用户ID</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.user_id') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ userDetail.id }}</span> <span class="text-gray-900 dark:text-dk-text">{{ userDetail.id }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">用户名</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.name') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ userDetail.name }}</span> <span class="text-gray-900 dark:text-dk-text">{{ userDetail.name }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">注册时间</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.table_created_at') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ new Date(userDetail.date).toLocaleString() }}</span> <span class="text-gray-900 dark:text-dk-text">{{ new Date(userDetail.date).toLocaleString() }}</span>
</div> </div>
<!-- 用户扩展信息 --> <!-- 用户扩展信息 -->
<template v-if="userDetailInfo"> <template v-if="userDetailInfo">
<hr class="border-gray-200 dark:border-dk-muted" /> <hr class="border-gray-200 dark:border-dk-muted" />
<div class="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">扩展信息</div> <div class="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.extended_info') }}</div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">昵称</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.info_nickname') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ userDetailInfo.username || '-' }}</span> <span class="text-gray-900 dark:text-dk-text">{{ userDetailInfo.username || '-' }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">备注</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.info_remark') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ userDetailInfo.firstname || '-' }}</span> <span class="text-gray-900 dark:text-dk-text">{{ userDetailInfo.firstname || '-' }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">生日</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.birthday') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ formatDate(userDetailInfo.birthdate) }}</span> <span class="text-gray-900 dark:text-dk-text">{{ formatDate(userDetailInfo.birthdate) }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">性别</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.gender') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ formatGender(userDetailInfo.gender) }}</span> <span class="text-gray-900 dark:text-dk-text">{{ formatGender(userDetailInfo.gender) }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">地区</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.region') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ userDetailInfo.region || '-' }}</span> <span class="text-gray-900 dark:text-dk-text">{{ userDetailInfo.region || '-' }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-dk-subtle">语言</span> <span class="text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.language') }}</span>
<span class="text-gray-900 dark:text-dk-text">{{ userDetailInfo.language || '-' }}</span> <span class="text-gray-900 dark:text-dk-text">{{ userDetailInfo.language || '-' }}</span>
</div> </div>
</template> </template>
@@ -958,12 +957,12 @@ onMounted(() => {
<!-- 修改密码区域 --> <!-- 修改密码区域 -->
<div class="mt-4 space-y-3 border-t border-gray-200 pt-4 dark:border-dk-muted"> <div class="mt-4 space-y-3 border-t border-gray-200 pt-4 dark:border-dk-muted">
<div class="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">重置密码</div> <div class="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">{{ t('sysadmin.reset_password') }}</div>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
v-model="newPassword" v-model="newPassword"
type="password" type="password"
placeholder="输入新密码(至少6位)" :placeholder="t('sysadmin.new_password_placeholder')"
class="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text" class="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text"
/> />
<button <button
@@ -971,7 +970,7 @@ onMounted(() => {
:disabled="resetPasswordLoading || !newPassword" :disabled="resetPasswordLoading || !newPassword"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
> >
{{ resetPasswordLoading ? '修改中...' : '修改密码' }} {{ resetPasswordLoading ? t('sysadmin.resetting') : t('sysadmin.reset_password_btn') }}
</button> </button>
</div> </div>
</div> </div>
@@ -981,7 +980,7 @@ onMounted(() => {
@click="closeUserDetail" @click="closeUserDetail"
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-dk-muted dark:text-dk-text dark:hover:bg-dk-base" class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-dk-muted dark:text-dk-text dark:hover:bg-dk-base"
> >
关闭 {{ t('sysadmin.close') }}
</button> </button>
</div> </div>
</div> </div>
@@ -995,7 +994,7 @@ onMounted(() => {
> >
<div class="w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-dk-card"> <div class="w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-dk-card">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-dk-text">添加成员到 {{ selectedGroup?.name }}</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-dk-text">{{ t('sysadmin.add_member_title', { name: selectedGroup?.name }) }}</h3>
<button <button
@click="closeAddMemberDialog" @click="closeAddMemberDialog"
class="text-gray-400 hover:text-gray-600 dark:text-dk-subtle dark:hover:text-dk-text" class="text-gray-400 hover:text-gray-600 dark:text-dk-subtle dark:hover:text-dk-text"
@@ -1010,7 +1009,7 @@ onMounted(() => {
<input <input
v-model="addMemberSearch" v-model="addMemberSearch"
type="text" type="text"
placeholder="搜索用户名或邮箱..." :placeholder="t('sysadmin.search_user_placeholder')"
class="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text" class="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text"
@keyup.enter="searchUsersToAdd" @keyup.enter="searchUsersToAdd"
/> />
@@ -1019,7 +1018,7 @@ onMounted(() => {
:disabled="addMemberSearchLoading" :disabled="addMemberSearchLoading"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
> >
{{ addMemberSearchLoading ? '搜索中...' : '搜索' }} {{ addMemberSearchLoading ? t('sysadmin.searching') : t('sysadmin.search') }}
</button> </button>
</div> </div>
</div> </div>
@@ -1027,13 +1026,13 @@ onMounted(() => {
<!-- 搜索结果 --> <!-- 搜索结果 -->
<div class="max-h-64 overflow-y-auto"> <div class="max-h-64 overflow-y-auto">
<div v-if="addMemberSearchLoading" class="py-4 text-center text-gray-500 dark:text-dk-subtle"> <div v-if="addMemberSearchLoading" class="py-4 text-center text-gray-500 dark:text-dk-subtle">
搜索中... {{ t('sysadmin.searching') }}
</div> </div>
<div v-else-if="addMemberSearchResults.length === 0 && addMemberSearch" class="py-4 text-center text-gray-500 dark:text-dk-subtle"> <div v-else-if="addMemberSearchResults.length === 0 && addMemberSearch" class="py-4 text-center text-gray-500 dark:text-dk-subtle">
未找到匹配的用户 {{ t('sysadmin.no_search_results') }}
</div> </div>
<div v-else-if="!addMemberSearch" class="py-4 text-center text-gray-500 dark:text-dk-subtle"> <div v-else-if="!addMemberSearch" class="py-4 text-center text-gray-500 dark:text-dk-subtle">
输入关键词搜索用户 {{ t('sysadmin.search_hint') }}
</div> </div>
<div v-else class="space-y-2"> <div v-else class="space-y-2">
<div <div
@@ -1059,7 +1058,7 @@ onMounted(() => {
:disabled="addMemberLoading" :disabled="addMemberLoading"
class="rounded-md bg-green-600 px-3 py-1 text-xs font-medium text-white hover:bg-green-700 disabled:opacity-50" class="rounded-md bg-green-600 px-3 py-1 text-xs font-medium text-white hover:bg-green-700 disabled:opacity-50"
> >
添加 {{ t('sysadmin.add') }}
</button> </button>
</div> </div>
</div> </div>
@@ -1070,7 +1069,7 @@ onMounted(() => {
@click="closeAddMemberDialog" @click="closeAddMemberDialog"
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-dk-muted dark:text-dk-text dark:hover:bg-dk-base" class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-dk-muted dark:text-dk-text dark:hover:bg-dk-base"
> >
关闭 {{ t('sysadmin.close') }}
</button> </button>
</div> </div>
</div> </div>