@@ -41,7 +41,7 @@
- 已加载全部 {{ visibleList.length }} 条
+ All {{ visibleList.length }} loaded
@@ -65,28 +65,28 @@ import { DocumentCopy } from '@element-plus/icons-vue'
const router = useRouter()
const adminInfo = ref({})
const characterList = ref([])
-// 角色总数(/character/count)
+// Total character count (/character/count)
const characterCount = ref(0)
const PAGE_SIZE = 100
const loading = ref(false)
-// 后端是否已无更多数据
+// Whether the backend has no more data
const noMore = ref(false)
-// 筛选条件表单(提交后由后端筛选)
+// Search condition form (filtered by the backend after submit)
const searchForm = reactive({
character_name: '',
user_uu_hash: '',
uu_hash: '',
})
-// 当前生效的筛选条件,随每次 range 请求发给后端
+// Currently active search conditions, sent to the backend with every range request
const searchCondition = ref({})
-// 添加角色弹窗
+// Add character dialog
const dialogVisible = ref(false)
-// 编辑角色弹窗
+// Edit character dialog
const editVisible = ref(false)
const editRow = ref(null)
@@ -95,7 +95,8 @@ const canGetCharacter = computed(() => {
return Boolean(adminInfo.value.permission?.can_operate_character)
})
-// 同名角色:角色 uu_hash 与所属用户 uu_hash 相同,不允许通过角色接口删除,默认隐藏
+// Same-name character: the character uu_hash equals the owner user uu_hash; it cannot be deleted
+// through the character API, so it is hidden by default
const hideSameName = ref(true)
const isSameNameCharacter = (row) => row.UUHash === row.UserUUHash
const visibleList = computed(() => (
@@ -104,16 +105,16 @@ const visibleList = computed(() => (
: characterList.value
))
-// 勾选状态:以行 ID 记录
+// Selection state: tracked by row ID
const selectedIds = ref([])
const isSelected = (id) => selectedIds.value.includes(id)
-// 切换显示范围时清空勾选,避免选中了看不见的行
+// Clear the selection when the display scope changes so hidden rows are not left selected
watch(hideSameName, () => {
selectedIds.value = []
})
-// 全选判定基于当前显示的数据
+// Select-all check is based on the currently displayed data
const allSelected = computed(
() => visibleList.value.length > 0 && visibleList.value.every((row) => isSelected(row.ID))
)
@@ -131,12 +132,12 @@ const toggleAll = () => {
: [...new Set([...selectedIds.value, ...shownIds])]
}
-// 取消全部选中
+// Clear all selections
const handleCheckboxCancel = () => {
selectedIds.value = []
}
-// 查询:把筛选条件交给后端,重新从第一页取
+// Search: hand the conditions to the backend and re-fetch from the first page
const handleSearch = () => {
searchCondition.value = {
character_name: searchForm.character_name.trim(),
@@ -146,7 +147,7 @@ const handleSearch = () => {
range(1)
}
-// 重置:清空筛选条件并重新从第一页取全部
+// Reset: clear the conditions and re-fetch everything from the first page
const handleReset = () => {
searchForm.character_name = ''
searchForm.user_uu_hash = ''
@@ -155,35 +156,36 @@ const handleReset = () => {
range(1)
}
-// 打开添加角色弹窗
+// Open the add character dialog
const handleAdd = () => {
dialogVisible.value = true
}
-// 提交添加:调用 /character/add,成功后刷新列表
+// Submit add: call /character/add, then refresh the list on success
const handleAddSubmit = async (form) => {
try {
- // 服务器关闭多角色开关时角色接口会返回 403,这里自己给提示,所以关掉统一错误提示
+ // The character API returns 403 when the server has the multi-character switch turned off.
+ // We show our own message here, so the shared error message is disabled
await post('/character/add', {
character_name: form.character_name.trim(),
password: form.password,
user_uu_hash: form.user_uu_hash.trim(),
meta_data: form.meta_data,
}, { autoRedirect401: false, showError: false })
- ElMessage.success('添加角色成功')
+ ElMessage.success('Character added')
dialogVisible.value = false
refresh()
fetchCount()
} catch (err) {
if (err?.body?.code === 403) {
- ElMessage.warning('服务器未开启多角色,不允许添加角色')
+ ElMessage.warning('Multi-character is not enabled on the server; adding characters is not allowed')
} else {
- ElMessage.error(err?.message || '添加角色失败')
+ ElMessage.error(err?.message || 'Add character failed')
}
}
}
-// 执行删除并同步列表(后端要求同时提交所属用户与角色的 uu_hash,故逐条调用)
+// Delete and sync the list (the backend requires both the owner user and character uu_hash, so call it row by row)
const deleteCharacters = async (rows) => {
const results = await Promise.allSettled(
rows.map((item) => del('/character/delete', {
@@ -193,14 +195,14 @@ const deleteCharacters = async (rows) => {
)
const successIds = rows.filter((_, index) => results[index].status === 'fulfilled').map((item) => item.ID)
const failed = results.filter((result) => result.status === 'rejected')
- // 成功的行取消勾选
+ // Uncheck the rows that were deleted successfully
selectedIds.value = selectedIds.value.filter((id) => !successIds.includes(id))
if (failed.length === 0) {
- ElMessage.success(`删除成功,共 ${successIds.length} 条`)
+ ElMessage.success(`Deleted, ${successIds.length} items`)
} else if (successIds.length === 0) {
- ElMessage.error(failed[0].reason?.message || '删除失败')
+ ElMessage.error(failed[0].reason?.message || 'Delete failed')
} else {
- ElMessage.warning(`成功 ${successIds.length} 条,失败 ${failed.length} 条:${failed[0].reason?.message ?? '未知原因'}`)
+ ElMessage.warning(`Succeeded ${successIds.length}, failed ${failed.length}: ${failed[0].reason?.message ?? 'Unknown reason'}`)
}
if (successIds.length > 0) {
refresh()
@@ -208,45 +210,45 @@ const deleteCharacters = async (rows) => {
}
}
-// 单个删除:确认后删除该行
+// Single delete: delete the row after confirmation
const handleDelete = async (row) => {
try {
- await ElMessageBox.confirm(`确定删除角色"${row.CharacterName}"吗?`, '删除确认', {
+ await ElMessageBox.confirm(`Delete character "${row.CharacterName}"?`, 'Delete Confirmation', {
type: 'warning',
- confirmButtonText: '确定',
- cancelButtonText: '取消',
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
})
} catch {
- // 取消删除
+ // Delete cancelled
return
}
deleteCharacters([row])
}
-// 批量删除:确认后删除所有选中行
+// Batch delete: delete all selected rows after confirmation
const handleBatchDelete = async () => {
const rows = visibleList.value.filter((item) => selectedIds.value.includes(item.ID))
if (rows.length === 0) return
try {
- await ElMessageBox.confirm(`确定删除选中的 ${rows.length} 条角色吗?`, '删除确认', {
+ await ElMessageBox.confirm(`Delete ${rows.length} selected characters?`, 'Delete Confirmation', {
type: 'warning',
- confirmButtonText: '确定',
- cancelButtonText: '取消',
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
})
} catch {
- // 取消删除
+ // Delete cancelled
return
}
deleteCharacters(rows)
}
-// 打开编辑角色弹窗
+// Open the edit character dialog
const handleEdit = (row) => {
editRow.value = row
editVisible.value = true
}
-// 提交编辑:调用 /character/edit,成功后刷新列表
+// Submit edit: call /character/edit, then refresh the list on success
const handleEditSubmit = async (form) => {
try {
await put('/character/edit', {
@@ -256,38 +258,40 @@ const handleEditSubmit = async (form) => {
password: form.password,
meta_data: form.meta_data,
}, { autoRedirect401: false })
- ElMessage.success('修改角色成功')
+ ElMessage.success('Character updated')
editVisible.value = false
refresh()
} catch {
- // 失败提示已由 request 封装统一弹出
+ // Error messages are already shown by the request wrapper
}
}
-// 复制文本到剪贴板
+// Copy text to the clipboard
const handleCopy = async (text) => {
try {
await navigator.clipboard.writeText(text)
- ElMessage.success('已复制')
+ ElMessage.success('Copied')
} catch {
- ElMessage.error('复制失败')
+ ElMessage.error('Copy failed')
}
}
-// UUHash 列统一渲染:过长交给 el-text 自动省略号(截断时会自带 title),右侧是复制按钮。
-// 行本身必须约束宽度并允许收缩,否则文字不省略、复制按钮会被单元格裁掉
+// Shared renderer for UUHash columns: el-text adds the ellipsis automatically when too long
+// (it also provides a title when truncated), with a copy button on the right.
+// The row itself must be width-constrained and allowed to shrink, otherwise the text is not
+// truncated and the copy button gets clipped by the cell
const hashCell = (key) => ({ rowData }) => h('div', {
style: 'display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0;',
}, [
h(ElText, { truncated: true, style: 'flex: 1; min-width: 0;' }, () => rowData[key]),
h(ElButton, {
size: 'small',
- title: '复制',
+ title: 'Copy',
onClick: () => handleCopy(rowData[key]),
}, () => h(ElIcon, null, () => h(DocumentCopy))),
])
-// MetaData 可能很长,单元格内截断显示,hover 看全文
+// MetaData can be long; truncate it in the cell and show the full text on hover
const metaDataCell = ({ rowData }) => h('span', {
style: 'display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;',
title: rowData.MetaData ?? '',
@@ -309,17 +313,17 @@ const columns = [
}),
},
{ key: 'ID', dataKey: 'ID', title: 'ID', width: 70 },
- { key: 'CharacterName', dataKey: 'CharacterName', title: '角色名', width: 160 },
- { key: 'UUHash', title: '角色UUHash', width: 260, cellRenderer: hashCell('UUHash') },
- { key: 'UserUUHash', title: '所属用户UUHash', width: 260, cellRenderer: hashCell('UserUUHash') },
+ { key: 'CharacterName', dataKey: 'CharacterName', title: 'Character Name', width: 160 },
+ { key: 'UUHash', title: 'Character UUHash', width: 260, cellRenderer: hashCell('UUHash') },
+ { key: 'UserUUHash', title: 'Owner User UUHash', width: 260, cellRenderer: hashCell('UserUUHash') },
{ key: 'MetaData', title: 'MetaData', width: 300, cellRenderer: metaDataCell },
{
key: 'actions',
- title: '操作',
- width: 200,
+ title: 'Actions',
+ width: 250,
cellRenderer: ({ rowData }) => h('div', { style: 'display: flex; gap: 8px;' }, [
- h(ElButton, { type: 'danger', size: 'small', onClick: () => handleDelete(rowData) }, () => '删除角色'),
- h(ElButton, { type: 'primary', size: 'small', onClick: () => handleEdit(rowData) }, () => '编辑角色'),
+ h(ElButton, { type: 'danger', size: 'small', onClick: () => handleDelete(rowData) }, () => 'Delete Character'),
+ h(ElButton, { type: 'primary', size: 'small', onClick: () => handleEdit(rowData) }, () => 'Edit Character'),
]),
},
]
@@ -329,7 +333,7 @@ const range = async (start = 1) => {
loading.value = true
try {
const list = await rangeCharacter(searchCondition.value, start, PAGE_SIZE)
- // refresh() 之后继续下拉时,请求的页可能与已加载数据重叠,按 ID 去重
+ // When scrolling further after refresh(), the requested page may overlap with already loaded data, so dedupe by ID
const map = new Map()
for (const row of start === 1 ? list : [...characterList.value, ...list]) {
map.set(row.ID, row)
@@ -337,14 +341,14 @@ const range = async (start = 1) => {
characterList.value = [...map.values()].sort((a, b) => a.ID - b.ID)
noMore.value = list.length < PAGE_SIZE
} catch {
- // 401 已由 request 封装自动跳转登录页
+ // 401 is already handled by the request wrapper, which redirects to the login page
} finally {
loading.value = false
}
}
-// 增删改后刷新:按当前已加载的行数重新取。
-// 不能只取第一页,否则已加载的那部分行会被丢掉,被操作的行看上去“消失”了
+// Refresh after add/edit/delete: re-fetch based on the currently loaded row count.
+// Fetching only the first page would drop already loaded rows, making the affected row appear to "disappear"
const refresh = async () => {
if (loading.value) return
loading.value = true
@@ -354,25 +358,25 @@ const refresh = async () => {
characterList.value = list.sort((a, b) => a.ID - b.ID)
noMore.value = list.length < length
} catch {
- // 401 已由 request 封装自动跳转登录页
+ // 401 is already handled by the request wrapper, which redirects to the login page
} finally {
loading.value = false
}
}
-// 滚动到底部:后端还有数据时继续请求下一页 100 行
+// Reached the bottom: keep requesting the next 100 rows while the backend has more data
const handleEndReached = () => {
if (loading.value || noMore.value) return
range(Math.floor(characterList.value.length / PAGE_SIZE) + 1)
}
-// 角色总数
+// Total character count
const fetchCount = async () => {
try {
const res = await post('/character/count')
characterCount.value = res.data.character_count
} catch {
- // 401 已由 request 封装自动跳转登录页
+ // 401 is already handled by the request wrapper, which redirects to the login page
}
}
@@ -381,7 +385,7 @@ onMounted(async () => {
const res = await post('/admin/info')
adminInfo.value = res.data
} catch {
- // 401 已由 request 封装自动跳转登录页,网络错误时不再继续鉴权
+ // 401 is already handled by the request wrapper, which redirects to the login page; skip further checks on network errors
return
}
if (!canGetCharacter.value) {
diff --git a/GAHFrontend/src/components/main/page/home.vue b/GAHFrontend/src/components/main/page/home.vue
index 0bb6de0..9168ae5 100644
--- a/GAHFrontend/src/components/main/page/home.vue
+++ b/GAHFrontend/src/components/main/page/home.vue
@@ -2,35 +2,37 @@
- 欢迎使用 GoAccountHub 管理后台
+ Welcome to GoAccountHub Admin
- 当前登录用户:{{ adminInfo.username || adminName || '未知' }}
+ Logged in as: {{ adminInfo.username || adminName || 'Unknown' }}
- 系统信息
+ System Info
- 管理员数量:{{ appInfo.admin_count || '未知' }}
- 用户数量:{{ appInfo.user_count || '未知' }}
- 角色数量:{{ appInfo.character_count || '未知' }}
- 总token数量:{{ appInfo.total_token_count || '未知' }}
- 管理员token数量:{{ appInfo.admin_token_count || '未知' }}
- 用户token数量:{{ appInfo.user_token_count || '未知' }}
+ Admin Count: {{ appInfo.admin_count || 'Unknown' }}
+ User Count: {{ appInfo.user_count || 'Unknown' }}
+ Character Count: {{ appInfo.character_count || 'Unknown' }}
+ Key Count: {{ appInfo.key_count ?? 'Unknown' }}
+ Total Tokens: {{ appInfo.total_token_count || 'Unknown' }}
+ Admin Tokens: {{ appInfo.admin_token_count || 'Unknown' }}
+ User Tokens: {{ appInfo.user_token_count || 'Unknown' }}
- 权限
+ Permissions
- 添加管理员:
- 删除管理员:
- 修改管理员信息:
- 查看管理员信息:
-
+ Add Admin:
+ Delete Admin:
+ Edit Admin Info:
+ View Admin Info:
+ Operate App Key:
+
@@ -42,7 +44,7 @@ import { onMounted, ref } from 'vue'
import { CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import { post } from '../../../lib/request'
-// 从 document.cookie 字符串中解析 admin_name(接口未返回前的兜底显示)
+// Parse admin_name from the document.cookie string (fallback display before the API returns)
const adminName = ref('')
const match = document.cookie.match(/(?:^|;\s*)admin_name=([^;]*)/)
if (match) {
@@ -52,7 +54,7 @@ if (match) {
const appInfo = ref({})
const adminInfo = ref({})
-// 权限判断:root 全部放行;普通管理员读 permission 字段;数据未加载时返回 false
+// Permission check: root is always allowed; regular admins read the permission field; returns false while data is not loaded
const hasPerm = (key) => {
if (adminInfo.value.is_root) return true
return Boolean(adminInfo.value.permission?.[key])
diff --git a/GAHFrontend/src/components/main/page/key.vue b/GAHFrontend/src/components/main/page/key.vue
new file mode 100644
index 0000000..fa057bd
--- /dev/null
+++ b/GAHFrontend/src/components/main/page/key.vue
@@ -0,0 +1,290 @@
+
+
+
+ Actions
+
+
+
+
+ Search
+ Reset
+ Add Key
+
+
+
+
+
+
+
+ Batch Actions
+
+ Selected {{ selectedIds.length }} items
+
+ Clear Selection
+ Delete Selected Keys
+
+
+
+
+
+
+ Key List
+
+
+
+
+
+
+ All {{ keyList.length }} loaded
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GAHFrontend/src/components/main/page/user.vue b/GAHFrontend/src/components/main/page/user.vue
index 383ffc9..a483fe9 100644
--- a/GAHFrontend/src/components/main/page/user.vue
+++ b/GAHFrontend/src/components/main/page/user.vue
@@ -1,15 +1,15 @@
- 操作
+ Actions
-
-
+
+
- 查询
- 重置
- 添加用户
+ Search
+ Reset
+ Add User
@@ -17,19 +17,19 @@
- 批量操作
+ Batch Actions
- 已选中 {{ selectedIds.length }} 条
+ Selected {{ selectedIds.length }} items
- 取消选中
- 删除选中用户
+ Clear Selection
+ Delete Selected Users
- 用户列表(共 {{ userCount }} 位)
+ User List ({{ userCount }} total)
@@ -37,7 +37,7 @@
- 已加载全部 {{ userList.length }} 条
+ All {{ userList.length }} loaded
@@ -61,27 +61,27 @@ import { DocumentCopy } from '@element-plus/icons-vue'
const router = useRouter()
const adminInfo = ref({})
const userList = ref([])
-// 用户总数(/user/count)
+// Total user count (/user/count)
const userCount = ref(0)
const PAGE_SIZE = 100
const loading = ref(false)
-// 后端是否已无更多数据
+// Whether the backend has no more data
const noMore = ref(false)
-// 筛选条件表单(提交后由后端筛选)
+// Search condition form (filtered by the backend after submit)
const searchForm = reactive({
username: '',
uu_hash: '',
})
-// 当前生效的筛选条件,随每次 range 请求发给后端
+// Currently active search conditions, sent to the backend with every range request
const searchCondition = ref({})
-// 添加用户弹窗
+// Add user dialog
const dialogVisible = ref(false)
-// 编辑用户弹窗
+// Edit user dialog
const editVisible = ref(false)
const editRow = ref(null)
@@ -90,10 +90,10 @@ const canGetUser = computed(() => {
return Boolean(adminInfo.value.permission?.can_operate_user)
})
-// 勾选状态:以行 ID 记录
+// Selection state: tracked by row ID
const selectedIds = ref([])
const isSelected = (id) => selectedIds.value.includes(id)
-// 全选判定基于当前数据
+// Select-all check is based on the current data
const allSelected = computed(
() => userList.value.length > 0 && userList.value.every((row) => isSelected(row.ID))
)
@@ -111,12 +111,12 @@ const toggleAll = () => {
: [...new Set([...selectedIds.value, ...shownIds])]
}
-// 取消全部选中
+// Clear all selections
const handleCheckboxCancel = () => {
selectedIds.value = []
}
-// 查询:把筛选条件交给后端,重新从第一页取
+// Search: hand the conditions to the backend and re-fetch from the first page
const handleSearch = () => {
searchCondition.value = {
username: searchForm.username.trim(),
@@ -125,7 +125,7 @@ const handleSearch = () => {
range(1)
}
-// 重置:清空筛选条件并重新从第一页取全部
+// Reset: clear the conditions and re-fetch everything from the first page
const handleReset = () => {
searchForm.username = ''
searchForm.uu_hash = ''
@@ -133,12 +133,12 @@ const handleReset = () => {
range(1)
}
-// 打开添加用户弹窗
+// Open the add user dialog
const handleAdd = () => {
dialogVisible.value = true
}
-// 提交添加:调用 /user/add,成功后刷新列表
+// Submit add: call /user/add, then refresh the list on success
const handleAddSubmit = async (form) => {
try {
await post('/user/add', {
@@ -146,30 +146,30 @@ const handleAddSubmit = async (form) => {
password: form.password,
meta_data: form.meta_data,
}, { autoRedirect401: false })
- ElMessage.success('添加用户成功')
+ ElMessage.success('User added')
dialogVisible.value = false
refresh()
fetchCount()
} catch {
- // 失败提示已由 request 封装统一弹出
+ // Error messages are already shown by the request wrapper
}
}
-// 执行删除并同步列表(后端每次只接收一个 uu_hash,故逐条调用)
+// Delete and sync the list (the backend accepts only one uu_hash at a time, so call it row by row)
const deleteUsers = async (rows) => {
const results = await Promise.allSettled(
rows.map((item) => del('/user/delete', { uu_hash: item.UUHash }, { autoRedirect401: false, showError: false }))
)
const successIds = rows.filter((_, index) => results[index].status === 'fulfilled').map((item) => item.ID)
const failed = results.filter((result) => result.status === 'rejected')
- // 成功的行取消勾选
+ // Uncheck the rows that were deleted successfully
selectedIds.value = selectedIds.value.filter((id) => !successIds.includes(id))
if (failed.length === 0) {
- ElMessage.success(`删除成功,共 ${successIds.length} 条`)
+ ElMessage.success(`Deleted, ${successIds.length} items`)
} else if (successIds.length === 0) {
- ElMessage.error(failed[0].reason?.message || '删除失败')
+ ElMessage.error(failed[0].reason?.message || 'Delete failed')
} else {
- ElMessage.warning(`成功 ${successIds.length} 条,失败 ${failed.length} 条:${failed[0].reason?.message ?? '未知原因'}`)
+ ElMessage.warning(`Succeeded ${successIds.length}, failed ${failed.length}: ${failed[0].reason?.message ?? 'Unknown reason'}`)
}
if (successIds.length > 0) {
refresh()
@@ -177,45 +177,45 @@ const deleteUsers = async (rows) => {
}
}
-// 单个删除:确认后删除该行
+// Single delete: delete the row after confirmation
const handleDelete = async (row) => {
try {
- await ElMessageBox.confirm(`确定删除用户"${row.Username}"吗?`, '删除确认', {
+ await ElMessageBox.confirm(`Delete user "${row.Username}"?`, 'Delete Confirmation', {
type: 'warning',
- confirmButtonText: '确定',
- cancelButtonText: '取消',
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
})
} catch {
- // 取消删除
+ // Delete cancelled
return
}
deleteUsers([row])
}
-// 批量删除:确认后删除所有选中行
+// Batch delete: delete all selected rows after confirmation
const handleBatchDelete = async () => {
const rows = userList.value.filter((item) => selectedIds.value.includes(item.ID))
if (rows.length === 0) return
try {
- await ElMessageBox.confirm(`确定删除选中的 ${rows.length} 条用户吗?`, '删除确认', {
+ await ElMessageBox.confirm(`Delete ${rows.length} selected users?`, 'Delete Confirmation', {
type: 'warning',
- confirmButtonText: '确定',
- cancelButtonText: '取消',
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
})
} catch {
- // 取消删除
+ // Delete cancelled
return
}
deleteUsers(rows)
}
-// 打开编辑用户弹窗
+// Open the edit user dialog
const handleEdit = (row) => {
editRow.value = row
editVisible.value = true
}
-// 提交编辑:调用 /user/edit,成功后刷新列表
+// Submit edit: call /user/edit, then refresh the list on success
const handleEditSubmit = async (form) => {
try {
await put('/user/edit', {
@@ -224,25 +224,25 @@ const handleEditSubmit = async (form) => {
password: form.password,
meta_data: form.meta_data,
}, { autoRedirect401: false })
- ElMessage.success('修改用户成功')
+ ElMessage.success('User updated')
editVisible.value = false
refresh()
} catch {
- // 失败提示已由 request 封装统一弹出
+ // Error messages are already shown by the request wrapper
}
}
-// 复制文本到剪贴板
+// Copy text to the clipboard
const handleCopy = async (text) => {
try {
await navigator.clipboard.writeText(text)
- ElMessage.success('已复制')
+ ElMessage.success('Copied')
} catch {
- ElMessage.error('复制失败')
+ ElMessage.error('Copy failed')
}
}
-// MetaData 可能很长,单元格内截断显示,hover 看全文
+// MetaData can be long; truncate it in the cell and show the full text on hover
const metaDataCell = ({ rowData }) => h('span', {
style: 'display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;',
title: rowData.MetaData ?? '',
@@ -264,7 +264,7 @@ const columns = [
}),
},
{ key: 'ID', dataKey: 'ID', title: 'ID', width: 70 },
- { key: 'Username', dataKey: 'Username', title: '用户名', width: 140 },
+ { key: 'Username', dataKey: 'Username', title: 'Username', width: 140 },
{
key: 'UUHash',
title: 'UUHash',
@@ -273,7 +273,7 @@ const columns = [
h('span', rowData.UUHash),
h(ElButton, {
size: 'small',
- title: '复制 UUHash',
+ title: 'Copy UUHash',
onClick: () => handleCopy(rowData.UUHash),
}, () => h(ElIcon, null, () => h(DocumentCopy))),
]),
@@ -281,11 +281,11 @@ const columns = [
{ key: 'MetaData', title: 'MetaData', width: 300, cellRenderer: metaDataCell },
{
key: 'actions',
- title: '操作',
+ title: 'Actions',
width: 200,
cellRenderer: ({ rowData }) => h('div', { style: 'display: flex; gap: 8px;' }, [
- h(ElButton, { type: 'danger', size: 'small', onClick: () => handleDelete(rowData) }, () => '删除用户'),
- h(ElButton, { type: 'primary', size: 'small', onClick: () => handleEdit(rowData) }, () => '编辑用户'),
+ h(ElButton, { type: 'danger', size: 'small', onClick: () => handleDelete(rowData) }, () => 'Delete User'),
+ h(ElButton, { type: 'primary', size: 'small', onClick: () => handleEdit(rowData) }, () => 'Edit User'),
]),
},
]
@@ -295,7 +295,7 @@ const range = async (start = 1) => {
loading.value = true
try {
const list = await rangeUser(searchCondition.value, start, PAGE_SIZE)
- // refresh() 之后继续下拉时,请求的页可能与已加载数据重叠,按 ID 去重
+ // When scrolling further after refresh(), the requested page may overlap with already loaded data, so dedupe by ID
const map = new Map()
for (const row of start === 1 ? list : [...userList.value, ...list]) {
map.set(row.ID, row)
@@ -303,14 +303,14 @@ const range = async (start = 1) => {
userList.value = [...map.values()].sort((a, b) => a.ID - b.ID)
noMore.value = list.length < PAGE_SIZE
} catch {
- // 401 已由 request 封装自动跳转登录页
+ // 401 is already handled by the request wrapper, which redirects to the login page
} finally {
loading.value = false
}
}
-// 增删改后刷新:按当前已加载的行数重新取。
-// 不能只取第一页,否则已加载的那部分行会被丢掉,被操作的行看上去“消失”了
+// Refresh after add/edit/delete: re-fetch based on the currently loaded row count.
+// Fetching only the first page would drop already loaded rows, making the affected row appear to "disappear"
const refresh = async () => {
if (loading.value) return
loading.value = true
@@ -320,25 +320,25 @@ const refresh = async () => {
userList.value = list.sort((a, b) => a.ID - b.ID)
noMore.value = list.length < length
} catch {
- // 401 已由 request 封装自动跳转登录页
+ // 401 is already handled by the request wrapper, which redirects to the login page
} finally {
loading.value = false
}
}
-// 滚动到底部:后端还有数据时继续请求下一页 100 行
+// Reached the bottom: keep requesting the next 100 rows while the backend has more data
const handleEndReached = () => {
if (loading.value || noMore.value) return
range(Math.floor(userList.value.length / PAGE_SIZE) + 1)
}
-// 用户总数
+// Total user count
const fetchCount = async () => {
try {
const res = await post('/user/count')
userCount.value = res.data.user_count
} catch {
- // 401 已由 request 封装自动跳转登录页
+ // 401 is already handled by the request wrapper, which redirects to the login page
}
}
@@ -347,7 +347,7 @@ onMounted(async () => {
const res = await post('/admin/info')
adminInfo.value = res.data
} catch {
- // 401 已由 request 封装自动跳转登录页,网络错误时不再继续鉴权
+ // 401 is already handled by the request wrapper, which redirects to the login page; skip further checks on network errors
return
}
if (!canGetUser.value) {
diff --git a/GAHFrontend/src/lib/rangeKey.js b/GAHFrontend/src/lib/rangeKey.js
new file mode 100644
index 0000000..62c38cf
--- /dev/null
+++ b/GAHFrontend/src/lib/rangeKey.js
@@ -0,0 +1,12 @@
+import { post } from './request.js'
+
+const rangeKey = async (condition, start, length) => {
+ const res = await post('/key/range', {
+ begin_table_id: start,
+ length: length,
+ search_condition: condition
+ })
+ return res.data
+}
+
+export default rangeKey
diff --git a/GAHFrontend/src/lib/request.js b/GAHFrontend/src/lib/request.js
index 2e63082..a81405f 100644
--- a/GAHFrontend/src/lib/request.js
+++ b/GAHFrontend/src/lib/request.js
@@ -3,15 +3,15 @@ import { ElMessage } from 'element-plus'
const BASE_URL = import.meta.env.VITE_API_BASE || '/api/v1'
/**
- * 异步请求封装
+ * Async request wrapper
* @param {Object} options
- * @param {string} options.url 接口路径(相对 BASE_URL,如 '/info')
- * @param {string} [options.method] 请求方法,默认 GET
- * @param {Object} [options.params] query 参数(GET)
- * @param {Object} [options.data] 请求体(POST/PUT/DELETE 自动 JSON 序列化)
- * @param {boolean} [options.showError=true] 失败时是否自动弹出错误提示
- * @param {boolean} [options.autoRedirect401=true] 401 时是否自动跳转登录页
- * @returns {Promise