2026-07-19 22:55:24 +08:00
< script setup lang = "ts" >
import { onMounted , ref } from 'vue' ;
import { useTokenStore } from '@/stores/token' ;
import MessageBox from '@/components/MessageBox.vue' ;
import {
isAdmin as apiProfileIsAdmin ,
changePassword as apiProfileChangePassword ,
getToken as apiProfileGetToken ,
deleteToken as apiProfileDeleteToken ,
type TokenRow ,
} from '@/api/profile' ;
import {
get as apiAdminGet ,
add as apiAdminAdd ,
update as apiAdminUpdate ,
del as apiAdminDel ,
type AdminUserRow ,
} from '@/api/admin' ;
import TokenItem from '@/components/admin/TokenItem.vue' ;
import UserItem from '@/components/admin/UserItem.vue' ;
const token = useTokenStore ();
const messagebox = ref < InstanceType < typeof MessageBox > > ();
// tab control state
const activeTab = ref < number >( 1 );
const showUserTab = ref < boolean >( false );
// profile tab state
const profilePendingPassword = ref < string >( "" );
// token tab state
const tokenItems = ref < Map < string , TokenRow > > ( new Map ());
// user tab state
const userItems = ref < Map < string , AdminUserRow > > ( new Map ());
const pendingUsername = ref < string >( "" );
onMounted ( async () => {
// check admin status for user tab visibility
showUserTab . value = await apiProfileIsAdmin ( token . currentToken );
// auto-refresh both lists
await refreshTokenList ();
if ( showUserTab . value ) {
await refreshUserList ();
}
})
// region: Profile Tab
const changePassword = async () => {
const newpassword = profilePendingPassword . value ;
if ( newpassword === "" ) return ;
const rv = await apiProfileChangePassword ( token . currentToken , newpassword );
if ( rv ) {
messagebox . value ? . show ( "Operation OK." );
profilePendingPassword . value = "" ;
} else {
messagebox . value ? . show ( "An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data." );
}
}
// endregion
// region: Token Tab
const refreshTokenList = async () => {
// clean token items
tokenItems . value . clear ();
// fetch data
const rv = await apiProfileGetToken ( token . currentToken );
if ( typeof rv === 'undefined' ) {
console . error ( "fail to fetch token list" );
return ;
}
// add into dict for rendering
for ( const item of rv ) {
tokenItems . value . set ( item [ 1 ], item );
}
}
const deleteTokenItem = async ( uuid : string ) => {
const rv = await apiProfileDeleteToken ( token . currentToken , uuid );
if ( ! rv ) {
messagebox . value ? . show ( "A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data." );
return ;
}
// TODO: revoking the current session token (the isMe case) is allowed here,
// but the local tokenStore is not cleared afterwards. The client is left in
// an inconsistent state until the next failed API call or page reload.
// Faithfully preserved from legacy for now; revisit later.
tokenItems . value . delete ( uuid );
}
// endregion
// region: User List Tab
const refreshUserList = async () => {
// clean user items
userItems . value . clear ();
// fetch data
const rv = await apiAdminGet ( token . currentToken );
if ( typeof rv === 'undefined' ) {
console . error ( "fail to fetch user list" );
return ;
}
// add into dict for rendering
for ( const item of rv ) {
userItems . value . set ( item [ 0 ], item );
}
}
const addUser = async () => {
const username = pendingUsername . value ;
if ( username === "" ) return ;
const rv = await apiAdminAdd ( token . currentToken , username );
if ( typeof rv === 'undefined' ) {
messagebox . value ? . show ( "An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data." );
return ;
}
userItems . value . set ( rv [ 0 ], rv );
}
const deleteUser = async ( username : string ) => {
const rv = await apiAdminDel ( token . currentToken , username );
if ( ! rv ) {
messagebox . value ? . show ( "A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data." );
return ;
}
userItems . value . delete ( username );
}
const updateUser = async ( username : string , newPassword : string , newIsAdmin : boolean ) => {
const savedIsAdmin = userItems . value . get ( username ) ! [ 1 ];
const passwordArg = newPassword === "" ? undefined : newPassword ;
const isAdminArg = newIsAdmin === savedIsAdmin ? undefined : newIsAdmin ;
const rv = await apiAdminUpdate ( token . currentToken , username , passwordArg , isAdminArg );
if ( ! rv ) {
messagebox . value ? . show ( "An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data." );
return ;
}
// safely update data
userItems . value . get ( username ) ! [ 1 ] = newIsAdmin ;
}
// endregion
</ script >
2026-04-28 15:47:32 +08:00
< template >
2026-07-19 22:55:24 +08:00
< div class = "container" style = "margin-top: 20px;" >
< div class = "tabs" >
< ul >
< li : class = "{ 'is-active': activeTab === 1 }" @click ="activeTab = 1" >< a > My Profile </ a ></ li >
< li : class = "{ 'is-active': activeTab === 2 }" @click ="activeTab = 2" >< a > Manage Multi - login </ a ></ li >
< li v-if = "showUserTab" :class="{ 'is-active': activeTab === 3 }" @click="activeTab = 3" >< a > Manager User </ a ></ li >
</ ul >
</ div >
</ div >
< div v-show = "activeTab === 1" class="container" style="margin-top: 20px;" >
< h1 class = "title" > Change Password </ h1 >
< div class = "field has-addons" >
< div class = "control" >
< input v-model = "profilePendingPassword" class="input" type="password" >
</ div >
< div class = "control" @click ="changePassword" >
< a class = "button is-primary" >
< span class = "icon is-small" >< font-awesome-icon icon = "fas fa-key" ></ font-awesome-icon ></ span >
</ a >
</ div >
</ div >
</ div >
< div v-show = "activeTab === 2" class="container" style="margin-top: 20px;" >
< h1 class = "title" > Manage multi - login </ h1 >
< h2 class = "subtitle" > Manage the multi - login of the current account . You can forced logout some login in
there .</ h2 >
< div class = "control" @click ="refreshTokenList" >
< a class = "button is-primary" >
< span class = "icon is-small" >< font-awesome-icon icon = "fas fa-sync" ></ font-awesome-icon ></ span >
</ a >
</ div >
< div style = "display: flex; flex-flow: column; margin-top: 1.25rem;" >
< div v-for = "item in tokenItems.values()" :key="item[1]"
style = "display: flex; flex-flow: column; margin-top: 1.25rem;" >
< TokenItem :uuid = "item[1]" :ua = "item[3]" :ip = "item[4]"
: expire -on =" new Date ( item [ 2 ] * 1000 ) .toLocaleString ()" : is -me =" token.currentToken === item [ 1 ]"
@delete ="deleteTokenItem" />
</ div >
</ div >
</ div >
< div v-show = "activeTab === 3" class="container" style="margin-top: 20px;" >
< h1 class = "title" > User List </ h1 >
< div class = "button-list" >
< div class = "field has-addons" >
< div class = "control" >
< input v-model = "pendingUsername" class="input" type="text" >
</ div >
< div class = "control" @click ="addUser" >
< a class = "button is-primary" >
< span class = "icon is-small" >< font-awesome-icon icon = "fas fa-plus" ></ font-awesome-icon ></ span >
</ a >
</ div >
</ div >
< div class = "control" @click ="refreshUserList" >
< a class = "button is-primary" >
< span class = "icon is-small" >< font-awesome-icon icon = "fas fa-sync" ></ font-awesome-icon ></ span >
</ a >
</ div >
</ div >
< div style = "display: flex; flex-flow: column; margin-top: 1.25rem;" >
< div v-for = "item in userItems.values()" :key="item[0]"
style = "display: flex; flex-flow: column; margin-top: 1.25rem;" >
< UserItem :username = "item[0]" :is-admin = "item[1]" @delete ="deleteUser" @update ="updateUser" />
</ div >
</ div >
</ div >
< MessageBox ref = "messagebox" />
2026-04-28 15:47:32 +08:00
</ template >
< style scoped ></ style >