refactor(ui): Remove organization management pages and related i18n

- Delete ui/page/sys/org/ directory (org management UI)
    - Remove org-related navigation from routes.js and layout
    - Remove org translations from langs.json
    - Update dashboard to remove org references
master
veypi 3 weeks ago
parent 12c55a2997
commit b378c3c5c4

@ -21,40 +21,9 @@
"nav.dashboard": "Dashboard", "nav.dashboard": "Dashboard",
"nav.home": "Home", "nav.home": "Home",
"nav.oauth": "OAuth Apps", "nav.oauth": "OAuth Apps",
"nav.org": "Organizations",
"nav.roles": "Roles", "nav.roles": "Roles",
"nav.profile": "Profile", "nav.profile": "Profile",
"nav.users": "Users", "nav.users": "Users",
"org.code": "Code",
"org.create": "Create Organization",
"org.create_first": "Create Organization",
"org.created": "Created successfully",
"org.created_at": "Created At",
"org.delete_confirm": "Are you sure you want to delete this organization?",
"org.deleted": "Deleted successfully",
"org.desc_placeholder": "Enter organization description (optional)",
"org.description": "Description",
"org.detail": "Organization Detail",
"org.edit": "Edit Organization",
"org.feature_coming": "Feature coming soon",
"org.info": "Information",
"org.joined_at": "Joined At",
"org.max_members": "Max Members",
"org.member_removed": "Member removed",
"org.members": "Members",
"org.name": "Organization Name",
"org.name_placeholder": "Enter organization name",
"org.no_description": "No description",
"org.no_members": "No members yet",
"org.no_orgs": "No Organizations Found",
"org.no_orgs_desc": "Get started by creating your first organization.",
"org.remove_confirm": "Are you sure you want to remove this member?",
"org.remove_member": "Remove",
"org.required_fields": "Name and Code are required",
"org.search_placeholder": "Search by name, code...",
"org.updated": "Updated successfully",
"org.updated_at": "Updated At",
"org.you": "You",
"user.email": "Email", "user.email": "Email",
"user.profile": "User Profile", "user.profile": "User Profile",
"user.role": "Role", "user.role": "Role",
@ -89,40 +58,9 @@
"nav.dashboard": "仪表盘", "nav.dashboard": "仪表盘",
"nav.home": "首页", "nav.home": "首页",
"nav.oauth": "OAuth应用", "nav.oauth": "OAuth应用",
"nav.org": "组织管理",
"nav.roles": "角色管理", "nav.roles": "角色管理",
"nav.profile": "个人中心", "nav.profile": "个人中心",
"nav.users": "用户管理", "nav.users": "用户管理",
"org.code": "组织代码",
"org.create": "创建组织",
"org.create_first": "创建第一个组织",
"org.created": "创建成功",
"org.created_at": "创建时间",
"org.delete_confirm": "确定要删除该组织吗?",
"org.deleted": "删除成功",
"org.desc_placeholder": "请输入组织描述(可选)",
"org.description": "组织描述",
"org.detail": "组织详情",
"org.edit": "编辑组织",
"org.feature_coming": "功能即将推出",
"org.info": "基本信息",
"org.joined_at": "加入时间",
"org.max_members": "成员上限",
"org.member_removed": "成员已移除",
"org.members": "成员列表",
"org.name": "组织名称",
"org.name_placeholder": "请输入组织名称",
"org.no_description": "暂无描述",
"org.no_members": "暂无成员",
"org.no_orgs": "暂无组织",
"org.no_orgs_desc": "开始创建您的第一个组织吧",
"org.remove_confirm": "确定要移除此成员吗?",
"org.remove_member": "移除成员",
"org.required_fields": "名称和代码为必填项",
"org.search_placeholder": "搜索组织名称或代码",
"org.updated": "更新成功",
"org.updated_at": "更新时间",
"org.you": "你",
"user.email": "邮箱", "user.email": "邮箱",
"user.profile": "个人资料", "user.profile": "个人资料",
"user.role": "角色", "user.role": "角色",

@ -69,20 +69,6 @@
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.org-switcher {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
padding: 5px 10px;
border-radius: 4px;
transition: background 0.2s;
}
.org-switcher:hover {
background-color: var(--bg-color-tertiary);
}
.user-profile { .user-profile {
display: flex; display: flex;
align-items: center; align-items: center;
@ -123,14 +109,7 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<!-- Org Switcher --> <v-lang></v-lang>
<div class="org-switcher" @click="openOrgSwitch" v-if="currentOrg">
<i class="fas fa-building"></i>
<span>{{ currentOrg.name }}</span>
<i class="fas fa-chevron-down" style="font-size: 12px;"></i>
</div>
<v-lang></v-lang>
<!-- User Profile --> <!-- User Profile -->
<div class="user-profile" @click="goToProfile"> <div class="user-profile" @click="goToProfile">
@ -153,12 +132,10 @@
<script setup> <script setup>
collapsed = false; collapsed = false;
user = $env.$vbase.user; user = $env.$vbase.user;
currentOrg = $env.$vbase.currentOrg;
// Define Menu Items // Define Menu Items
menuItems = [ menuItems = [
{label: () => $t('nav.dashboard'), icon: "<i class='fas fa-tachometer-alt'></i>", path: "/"}, {label: () => $t('nav.dashboard'), icon: "<i class='fas fa-tachometer-alt'></i>", path: "/"},
{label: () => $t('nav.org'), icon: "<i class='fas fa-sitemap'></i>", path: "/org"},
{label: () => $t('nav.profile'), icon: "<i class='fas fa-user'></i>", path: "/profile"}, {label: () => $t('nav.profile'), icon: "<i class='fas fa-user'></i>", path: "/profile"},
// Admin only items would be filtered here ideally // Admin only items would be filtered here ideally
{label: () => $t('nav.users'), icon: "<i class='fas fa-users-cog'></i>", path: "/users"}, {label: () => $t('nav.users'), icon: "<i class='fas fa-users-cog'></i>", path: "/users"},
@ -183,12 +160,6 @@
goToProfile = () => { goToProfile = () => {
$router.push('/profile'); $router.push('/profile');
}; };
openOrgSwitch = () => {
// Simple alert for now, should be a modal or dropdown
// In vhtml we can use $message or navigate to org list
$router.push('/org');
};
</script> </script>
</html> </html>

@ -65,7 +65,7 @@
<script setup> <script setup>
stats = [ stats = [
{title: "Total Users", value: "1,234"}, {title: "Total Users", value: "1,234"},
{title: "Active Orgs", value: "56"}, {title: "Active Sessions", value: "56"},
{title: "API Calls", value: "89.2k"}, {title: "API Calls", value: "89.2k"},
{title: "Revenue", value: "$12,340"} {title: "Revenue", value: "$12,340"}
]; ];

@ -1,487 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Org Detail">
<title>{{ org ? org.name : $t('org.detail') }}</title>
</head>
<style>
body {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-md);
}
.header-left {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.btn-back {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: var(--radius-md);
background: var(--bg-color-tertiary);
color: var(--text-color);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-back:hover {
background: var(--border-color);
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
}
.section {
background: var(--bg-color-secondary);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
}
.section-title {
font-size: var(--font-size-lg);
font-weight: 600;
margin-bottom: var(--spacing-md);
color: var(--text-color);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
}
.info-item {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.info-label {
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
}
.info-value {
font-size: var(--font-size-md);
font-weight: 500;
color: var(--text-color);
}
.org-icon-large {
width: 64px;
height: 64px;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 28px;
font-weight: bold;
}
.org-header-info {
display: flex;
align-items: center;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
.org-header-text {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.org-header-name {
font-size: var(--font-size-2xl);
font-weight: 600;
color: var(--text-color);
}
.org-header-desc {
font-size: var(--font-size-md);
color: var(--text-color-secondary);
}
/* Members Table */
.members-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
th {
background-color: var(--bg-color-tertiary);
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
}
td {
font-size: var(--font-size-md);
color: var(--text-color);
}
tr:hover td {
background-color: var(--bg-color-tertiary);
}
.role-badge {
display: inline-flex;
align-items: center;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 500;
}
.role-admin {
background-color: var(--color-primary);
color: var(--color-primary-text);
}
.role-member {
background-color: var(--bg-color-tertiary);
color: var(--text-color-secondary);
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 500;
}
.status-active {
background-color: color-mix(in srgb, var(--color-success), transparent 85%);
color: var(--color-success);
}
.status-inactive {
background-color: color-mix(in srgb, var(--text-color-disabled), transparent 85%);
color: var(--text-color-secondary);
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
gap: var(--spacing-md);
color: var(--text-color-secondary);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-lg);
gap: var(--spacing-sm);
color: var(--text-color-secondary);
text-align: center;
}
.empty-state i {
font-size: 48px;
color: var(--border-color);
}
</style>
<body>
<!-- Loading State -->
<div class="loading-state" v-if="loading">
<div class="spinner"></div>
<span>{{ $t('common.loading') }}</span>
</div>
<div v-if="!loading && org">
<!-- Page Header -->
<div class="page-header">
<div class="header-left">
<button class="btn-back" @click="goBack" :title="$t('common.back')">
<i class="fas fa-arrow-left"></i>
</button>
<h1>{{ $t('org.detail') }}</h1>
</div>
<div class="header-actions">
<v-btn variant="outline" :click="openEditModal">
<i class="fas fa-edit"></i>
{{ $t('common.edit') }}
</v-btn>
<v-btn color="danger" :click="deleteOrg">
<i class="fas fa-trash"></i>
{{ $t('common.delete') }}
</v-btn>
</div>
</div>
<!-- Org Info Section -->
<div class="section">
<div class="org-header-info">
<div class="org-icon-large">{{ org.name ? org.name.charAt(0).toUpperCase() : 'O' }}</div>
<div class="org-header-text">
<div class="org-header-name">{{ org.name }}</div>
<div class="org-header-desc">{{ org.description || $t('org.no_description') }}</div>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">ID</span>
<span class="info-value">{{ org.id }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ $t('org.code') }}</span>
<span class="info-value">{{ org.code }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ $t('common.status') }}</span>
<span class="info-value">
<span class="status-badge" :class="org.status === 1 ? 'status-active' : 'status-inactive'">
{{ org.status === 1 ? 'Active' : 'Inactive' }}
</span>
</span>
</div>
<div class="info-item">
<span class="info-label">{{ $t('org.created_at') }}</span>
<span class="info-value">{{ formatDate(org.created_at) }}</span>
</div>
<div class="info-item" v-if="org.updated_at">
<span class="info-label">{{ $t('org.updated_at') }}</span>
<span class="info-value">{{ formatDate(org.updated_at) }}</span>
</div>
</div>
</div>
<!-- Members Section -->
<div class="section">
<div class="members-header">
<div class="section-title">
<i class="fas fa-users"></i>
{{ $t('org.members') }}
<span style="font-size: var(--font-size-sm); color: var(--text-color-tertiary);">({{ members.length }})</span>
</div>
</div>
<div class="empty-state" v-if="members.length === 0">
<i class="fas fa-user-slash"></i>
<p>{{ $t('org.no_members') }}</p>
</div>
<table v-if="members.length > 0">
<thead>
<tr>
<th>{{ $t('user.username') }}</th>
<th>{{ $t('user.email') }}</th>
<th>{{ $t('user.role') }}</th>
<th>{{ $t('common.status') }}</th>
<th>{{ $t('org.joined_at') }}</th>
<th>{{ $t('common.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="member in members">
<td>{{ member.username }}</td>
<td>{{ member.email || '-' }}</td>
<td>
<span class="role-badge" :class="member.role_ids === 'admin' ? 'role-admin' : 'role-member'">
{{ member.role_ids || 'member' }}
</span>
</td>
<td>
<span class="status-badge" :class="member.status === 1 ? 'status-active' : 'status-inactive'">
{{ member.status === 1 ? 'Active' : 'Inactive' }}
</span>
</td>
<td>{{ formatDate(member.joined_at) }}</td>
<td>
<v-btn size="sm" color="danger" variant="outline" :click="() => removeMember(member)"
v-if="member.id !== currentUserId">
<i class="fas fa-user-minus"></i>
{{ $t('org.remove_member') }}
</v-btn>
<span v-else style="color: var(--text-color-tertiary); font-size: var(--font-size-sm);">
{{ $t('org.you') }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Edit Dialog -->
<v-dialog v:visible="showEditModal" :title="$t('org.edit')">
<v-input type="text" v:value="editForm.name" :label="$t('org.name')" required
:placeholder="$t('org.name_placeholder')">
</v-input>
<v-input type="textarea" v:value="editForm.description" :label="$t('org.description')"
:placeholder="$t('org.desc_placeholder')">
</v-input>
<div vslot="footer">
<v-btn variant="outline" :click="closeEditModal">{{ $t('common.cancel') }}</v-btn>
<v-btn color="primary" :disabled="!editForm.name" :click="saveOrg">
{{ $t('common.save') }}
</v-btn>
</div>
</v-dialog>
</body>
<script setup>
orgId = $router.params.id;
org = null;
members = [];
totalMembers = 0;
loading = false;
currentUserId = $env.$vbase.user?.id;
// Edit modal state
showEditModal = false;
editForm = {
name: "",
description: ""
};
loadData = async () => {
loading = true;
try {
const [orgRes, membersRes] = await Promise.all([
$axios.get(`/api/orgs/${orgId}`),
$axios.get(`/api/orgs/${orgId}/members`)
]);
org = orgRes;
members = membersRes.items || [];
totalMembers = membersRes.total || 0;
} catch (e) {
$message.error(e.message);
if (e.status === 404) {
$router.push('/org');
}
} finally {
loading = false;
}
};
formatDate = (dateStr) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString();
};
goBack = () => {
$router.back();
};
// Edit modal
openEditModal = () => {
editForm = {
name: org.name,
description: org.description || ""
};
showEditModal = true;
};
closeEditModal = () => {
showEditModal = false;
};
saveOrg = async () => {
if (!editForm.name) return;
try {
await $axios.patch(`/api/orgs/${orgId}`, {
name: editForm.name,
description: editForm.description
});
$message.success($t('org.updated'));
closeEditModal();
loadData();
} catch (e) {
$message.error(e.message);
}
};
deleteOrg = async () => {
try {
await $message.confirm($t('org.delete_confirm'));
await $axios.delete(`/api/orgs/${orgId}`);
$message.success($t('org.deleted'));
$router.push('/org');
} catch (e) {
// Cancelled
}
};
removeMember = async (member) => {
try {
await $message.confirm($t('org.remove_confirm'));
// API might not support this yet, but prepare for it
await $axios.delete(`/api/orgs/${orgId}/members/${member.id}`);
$message.success($t('org.member_removed'));
loadData();
} catch (e) {
// Cancelled or not implemented
if (e.status === 404) {
$message.info($t('org.feature_coming'));
}
}
};
</script>
<script>
$data.loadData();
</script>
</html>

@ -1,472 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Org Management">
<title>{{ $t('nav.org') }}</title>
<style>
body {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
padding: var(--spacing-lg);
box-sizing: border-box;
overflow: hidden;
background-color: var(--bg-color);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-md);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.page-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--text-color);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.search-box {
display: flex;
align-items: center;
gap: var(--spacing-sm);
background: var(--bg-color-secondary);
padding: 0 var(--spacing-md);
border-radius: var(--radius-full);
border: 1px solid var(--border-color);
min-width: 320px;
height: 40px;
transition: all var(--transition-base);
}
.search-box:focus-within {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary), transparent 85%);
}
.search-box input {
border: none;
background: transparent;
outline: none;
font-size: var(--font-size-md);
color: var(--text-color);
width: 100%;
height: 100%;
}
.search-box input::placeholder {
color: var(--text-color-tertiary);
}
.org-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--spacing-lg);
overflow-y: auto;
padding: 4px; /* Prevent shadow clipping */
flex: 1;
}
.org-card {
background: var(--bg-color-secondary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
cursor: pointer;
transition: all var(--transition-base);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.org-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--color-primary);
}
.org-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing-md);
}
.org-icon {
width: 56px;
height: 56px;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
display: flex;
align-items: center;
justify-content: center;
color: var(--color-primary-text);
font-size: var(--font-size-xl);
font-weight: bold;
flex-shrink: 0;
overflow: hidden;
}
.org-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.org-info {
flex: 1;
min-width: 0;
}
.org-name {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-lg);
color: var(--text-color);
margin-bottom: var(--spacing-xs);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.org-code {
font-size: var(--font-size-xs);
color: var(--text-color-tertiary);
background: var(--bg-color-tertiary);
padding: 2px 6px;
border-radius: var(--radius-sm);
display: inline-block;
font-family: monospace;
}
.org-status {
position: absolute;
top: var(--spacing-md);
right: var(--spacing-md);
font-size: var(--font-size-xs);
padding: 2px 8px;
border-radius: var(--radius-full);
background: var(--bg-color-tertiary);
color: var(--text-color-secondary);
}
.org-status.active {
background: color-mix(in srgb, var(--color-success), transparent 85%);
color: var(--color-success);
}
.org-desc {
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
line-height: 1.6;
height: 42px; /* 2 lines */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: var(--spacing-xs);
}
.org-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--font-size-xs);
color: var(--text-color-tertiary);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-color);
margin-top: auto;
}
.org-meta-item {
display: flex;
align-items: center;
gap: 6px;
}
.org-actions {
display: flex;
gap: var(--spacing-sm);
opacity: 0;
transition: opacity var(--transition-fast);
position: absolute;
top: var(--spacing-md);
right: var(--spacing-md);
background: var(--bg-color-secondary);
padding-left: var(--spacing-sm);
}
.org-card:hover .org-actions {
opacity: 1;
}
/* Hide status when hovering if actions overlap, or better, move status */
.org-card:hover .org-status {
opacity: 0;
}
.loading-state, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
gap: var(--spacing-md);
color: var(--text-color-secondary);
flex: 1;
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-icon {
font-size: 64px;
color: var(--border-color);
margin-bottom: var(--spacing-md);
}
</style>
</head>
<body>
<div class="page-header">
<div class="page-title">
<i class="fas fa-sitemap" style="color: var(--color-primary);"></i>
{{ $t('nav.org') }}
</div>
<div style="display: flex; gap: var(--spacing-md); align-items: center;">
<div class="search-box">
<i class="fas fa-search" style="color: var(--text-color-tertiary);"></i>
<input type="text" v:value="searchQuery"
:placeholder="$t('org.search_placeholder')">
</div>
<v-btn color="primary" :click="openCreateModal">
<i class="fas fa-plus"></i>
{{ $t('org.create') }}
</v-btn>
</div>
</div>
<!-- Loading State -->
<div class="loading-state" v-if="loading">
<div class="spinner"></div>
<span>{{ $t('common.loading') }}</span>
</div>
<!-- Empty State -->
<div class="empty-state" v-if="!loading && filteredOrgs().length === 0">
<i class="fas fa-building empty-icon"></i>
<h3>{{ $t('org.no_orgs') }}</h3>
<p>{{ $t('org.no_orgs_desc') }}</p>
<v-btn color="primary" :click="openCreateModal" v-if="searchQuery === ''" style="margin-top: var(--spacing-md);">
<i class="fas fa-plus"></i>
{{ $t('org.create_first') }}
</v-btn>
</div>
<!-- Org Grid -->
<div class="org-grid" v-if="!loading && filteredOrgs().length > 0">
<div class="org-card" v-for="org in filteredOrgs()" @click="goToDetail(org.id)">
<div class="org-status" :class="org.status === 1 ? 'active' : ''">
{{ org.status === 1 ? 'Active' : 'Inactive' }}
</div>
<div class="org-header">
<div class="org-icon">
<img v-if="org.logo && org.logo.startsWith('http')" :src="org.logo" alt="logo" onerror="this.style.display='none'">
<span v-else>{{ org.name ? org.name.charAt(0).toUpperCase() : 'O' }}</span>
</div>
<div class="org-info">
<div class="org-name">{{ org.name }}</div>
<div class="org-code">{{ org.code }}</div>
</div>
</div>
<div class="org-desc">{{ org.description || $t('org.no_description') }}</div>
<div class="org-meta">
<div class="org-meta-item" title="Members Limit">
<i class="fas fa-users"></i>
<span>Max: {{ org.max_members }}</span>
</div>
<div class="org-meta-item" title="Created At">
<i class="fas fa-calendar-alt"></i>
<span>{{ formatDate(org.created_at) }}</span>
</div>
</div>
<div class="org-actions" @click.stop>
<v-btn icon size="sm" variant="outline" :click="() => openEditModal(org)" title="Edit">
<i class="fas fa-edit"></i>
</v-btn>
<v-btn icon size="sm" color="danger" variant="outline" :click="() => deleteOrg(org)" title="Delete">
<i class="fas fa-trash"></i>
</v-btn>
</div>
</div>
</div>
<!-- Create/Edit Dialog -->
<v-dialog v:visible="showModal"
:title="isEdit ? $t('org.edit') : $t('org.create')">
<form @submit.prevent="saveOrg" style="display: grid; gap: 16px;">
<v-input v-for="item in formItems" :type="item.type || 'text'" :label="$t(item.labelKey)"
:required="item.required" :placeholder="$t(item.placeholderKey)"
v:value="formData[item.name]" :disabled="item.name === 'code' && isEdit">
</v-input>
</form>
<div vslot="footer">
<v-btn variant="outline" :click="closeModal">{{ $t('common.cancel') }}</v-btn>
<v-btn color="primary" :click="saveOrg">
{{ isEdit ? $t('common.save') : $t('common.create') }}
</v-btn>
</div>
</v-dialog>
</body>
<script setup>
// State
orgs = [];
loading = false;
searchQuery = "";
showModal = false;
isEdit = false;
formData = {
id: null,
name: "",
code: "",
description: "",
logo: ""
};
formItems = [
{name: 'name', labelKey: 'org.name', label: 'Organization Name', required: true, placeholderKey: 'org.name_placeholder', placeholder: 'Enter organization name'},
{name: 'code', labelKey: 'org.code', label: 'Organization Code', required: true, placeholderKey: 'org.code_placeholder', placeholder: 'Enter organization code (unique)'},
{name: 'logo', labelKey: 'org.logo', label: 'Logo URL', required: true, placeholderKey: 'org.logo_placeholder', placeholder: 'Enter logo URL'},
{name: 'description', type: 'textarea', labelKey: 'org.description', label: 'Description', placeholderKey: 'org.desc_placeholder', placeholder: 'Enter organization description (optional)'}
];
// Helper: Format Date
formatDate = (dateStr) => {
if (!dateStr || dateStr.startsWith('0001')) return '-';
try {
const date = new Date(dateStr);
return date.toLocaleDateString();
} catch (e) {
return dateStr;
}
};
// Computed filtered orgs
filteredOrgs = () => {
if (!searchQuery) return orgs;
const query = searchQuery.toLowerCase();
return orgs.filter(org =>
org.name.toLowerCase().includes(query) ||
(org.description && org.description.toLowerCase().includes(query)) ||
(org.code && org.code.toLowerCase().includes(query))
);
};
// Load orgs
loadOrgs = async () => {
loading = true;
try {
const res = await $axios.get('/api/orgs');
orgs = res.items || [];
} catch (e) {
$message.error(e.message);
} finally {
loading = false;
}
};
// Modal operations
openCreateModal = () => {
isEdit = false;
formData = {id: null, name: "", code: "", description: "", logo: ""};
showModal = true;
};
openEditModal = (org) => {
isEdit = true;
formData = {...org};
showModal = true;
};
closeModal = () => {
showModal = false;
};
// Save (create or update)
saveOrg = async () => {
if (!formData.name || !formData.code) {
$message.error($t('org.required_fields'));
return;
}
try {
if (isEdit) {
await $axios.patch(`/api/orgs/${formData.id}`, {
name: formData.name,
description: formData.description,
logo: formData.logo
});
$message.success($t('org.updated'));
} else {
await $axios.post('/api/orgs', {
name: formData.name,
code: formData.code,
description: formData.description,
logo: formData.logo
});
$message.success($t('org.created'));
}
closeModal();
loadOrgs();
} catch (e) {
console.warn(e)
$message.error(e.message);
}
};
// Delete
deleteOrg = async (org) => {
try {
await $message.confirm($t('org.delete_confirm'));
await $axios.delete(`/api/orgs/${org.id}`);
$message.success($t('org.deleted'));
loadOrgs();
} catch (e) {
// Cancelled
}
};
// Navigate to detail
goToDetail = (id) => {
$router.push('/org/' + id);
};
</script>
<script>
$data.loadOrgs();
</script>
</html>

@ -11,15 +11,6 @@ const routes = [
component: '/page/dashboard/index.html' component: '/page/dashboard/index.html'
}, },
// Org Management
{ path: '/org', component: '/page/sys/org/index.html', layout: 'default', meta: { auth: true } },
{
path: '/org/:id',
component: '/page/sys/org/detail.html',
layout: 'default',
meta: { auth: true }
},
// Role Management // Role Management
{ path: '/roles', component: '/page/sys/role/index.html', layout: 'default', meta: { auth: true } }, { path: '/roles', component: '/page/sys/role/index.html', layout: 'default', meta: { auth: true } },

@ -15,19 +15,16 @@ class VBase {
this.tokenKey = 'vbase_access_token'; this.tokenKey = 'vbase_access_token';
this.refreshTokenKey = 'vbase_refresh_token'; this.refreshTokenKey = 'vbase_refresh_token';
this.userKey = 'vbase_user_info'; this.userKey = 'vbase_user_info';
this.orgKey = 'vbase_current_org';
this._token = localStorage.getItem(this.tokenKey) || ''; this._token = localStorage.getItem(this.tokenKey) || '';
this._refreshToken = localStorage.getItem(this.refreshTokenKey) || ''; this._refreshToken = localStorage.getItem(this.refreshTokenKey) || '';
this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null'); this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null');
this._currentOrg = JSON.parse(localStorage.getItem(this.orgKey) || 'null');
} }
// Getters // Getters
get token() { return this._token; } get token() { return this._token; }
get refreshToken() { return this._refreshToken; } get refreshToken() { return this._refreshToken; }
get user() { return this._user; } get user() { return this._user; }
get currentOrg() { return this._currentOrg; }
// Setters // Setters
set token(val) { set token(val) {
@ -48,12 +45,6 @@ class VBase {
else localStorage.removeItem(this.userKey); else localStorage.removeItem(this.userKey);
} }
set currentOrg(val) {
this._currentOrg = val;
if (val) localStorage.setItem(this.orgKey, JSON.stringify(val));
else localStorage.removeItem(this.orgKey);
}
// API Helpers // API Helpers
async request(method, path, data = null, headers = {}) { async request(method, path, data = null, headers = {}) {
const url = `${this.baseURL}${path}`; const url = `${this.baseURL}${path}`;
@ -144,9 +135,6 @@ class VBase {
if (this.token) { if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`; headers['Authorization'] = `Bearer ${this.token}`;
} }
if (this.currentOrg && this.currentOrg.id) {
headers['X-Org-ID'] = this.currentOrg.id;
}
return headers; return headers;
} }

Loading…
Cancel
Save