@k3000/ce 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -62
- package/app/comp/comp-test.mjs +24 -0
- package/{comp/new-form.mjs → app/comp/my-form.mjs} +2 -5
- package/app/comp/nav-bar.mjs +92 -0
- package/app/comp/page-container.mjs +55 -0
- package/app/comp/tab-bar.mjs +60 -0
- package/app/index.html +39 -0
- package/app/pages/not-found.mjs +7 -0
- package/app/pages/page-category.mjs +7 -0
- package/app/pages/page-home.mjs +14 -0
- package/app/pages/page-my.mjs +7 -0
- package/app/pages/page-test.mjs +22 -0
- package/comm/router-view.mjs +96 -0
- package/console/api/mock.mjs +211 -0
- package/console/components/common-action-button.mjs +63 -0
- package/console/components/common-header.mjs +26 -0
- package/console/components/common-input.mjs +44 -0
- package/console/components/common-modal.mjs +45 -0
- package/console/components/common-pagination.mjs +87 -0
- package/console/components/common-toolbar.mjs +39 -0
- package/console/components/console-header.mjs +26 -0
- package/console/components/layout-container.mjs +46 -0
- package/console/components/layout-header.mjs +179 -0
- package/console/components/layout-menu.mjs +221 -0
- package/console/index.html +50 -0
- package/console/pages/page-dashboard.mjs +15 -0
- package/console/pages/system-menu.mjs +179 -0
- package/console/pages/system-role.mjs +199 -0
- package/console/pages/system-user.mjs +199 -0
- package/index.mjs +854 -98
- package/package.json +1 -1
- package/comp/div-box.mjs +0 -20
- package/index.html +0 -17
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { getMenusByPage, deleteMenu, saveMenu, toggleMenuStatus } from "../api/mock.mjs";
|
|
2
|
+
|
|
3
|
+
export const innerHTML = `
|
|
4
|
+
<div class="h-full flex flex-col p-6 gap-6">
|
|
5
|
+
<!-- Header Component -->
|
|
6
|
+
<common-header title="菜单管理" description="配置系统导航菜单及权限标识">
|
|
7
|
+
<common-toolbar button-text="新增菜单" placeholder="搜索菜单..." value="{{searchQuery}}" onsearch="{{onSearch}}" onadd="{{onAdd}}"></common-toolbar>
|
|
8
|
+
</common-header>
|
|
9
|
+
|
|
10
|
+
<!-- Content Card -->
|
|
11
|
+
<div class="flex-1 bg-white/60 dark:bg-gray-800/60 backdrop-blur-2xl rounded-2xl shadow-lg shadow-gray-200/20 dark:shadow-black/20 border border-white/40 dark:border-white/10 overflow-hidden flex flex-col relative">
|
|
12
|
+
|
|
13
|
+
<!-- Table Section -->
|
|
14
|
+
<div class="flex-1 overflow-auto custom-scrollbar">
|
|
15
|
+
<table class="w-full text-left border-collapse">
|
|
16
|
+
<thead class="bg-gray-50/50 dark:bg-gray-700/30 sticky top-0 z-10 backdrop-blur-md border-b border-white/20 dark:border-white/5">
|
|
17
|
+
<tr>
|
|
18
|
+
<th each="{{ths}}" class="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider {{typeof scope === 'string' ? '' : classStr}}">
|
|
19
|
+
{{typeof scope === 'string' ? scope : title}}
|
|
20
|
+
</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody onclick="{{dispatchAction}}" class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
24
|
+
<tr each="{{menuList}}" class="hover:bg-white/40 dark:hover:bg-white/5 transition-colors group border-b border-transparent hover:border-white/20">
|
|
25
|
+
<td class="px-6 py-4 text-sm text-gray-500 font-mono">{{id}}</td>
|
|
26
|
+
<td class="px-6 py-4">
|
|
27
|
+
<div class="flex items-center gap-3">
|
|
28
|
+
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-100 to-indigo-100 dark:from-blue-900/30 dark:to-indigo-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 shadow-sm">
|
|
29
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
30
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{icon}}"></path>
|
|
31
|
+
</svg>
|
|
32
|
+
</div>
|
|
33
|
+
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{menuName}}</span>
|
|
34
|
+
</div>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="px-6 py-4">
|
|
37
|
+
<span class="px-2 py-1 rounded-md bg-gray-100 dark:bg-gray-800 text-xs font-mono text-gray-600 dark:text-gray-400 border border-gray-200/50 dark:border-gray-700/50">{{path}}</span>
|
|
38
|
+
</td>
|
|
39
|
+
<td class="px-6 py-4">
|
|
40
|
+
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium {{status === 'active' ? 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400' : 'bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400'}}">
|
|
41
|
+
<span class="w-1.5 h-1.5 rounded-full {{status === 'active' ? 'bg-green-500' : 'bg-red-500'}}"></span>
|
|
42
|
+
{{status === 'active' ? '正常' : '禁用'}}
|
|
43
|
+
</span>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="px-6 py-4 text-sm text-gray-500">{{sort}}</td>
|
|
46
|
+
<td class="px-6 py-4 text-sm text-gray-500">{{createdAt}}</td>
|
|
47
|
+
<td class="px-6 py-4 text-right">
|
|
48
|
+
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
49
|
+
<common-action-button action="toggle" id="{{id}}" title="{{status === 'active' ? '禁用' : '启用'}}"></common-action-button>
|
|
50
|
+
<common-action-button action="edit" id="{{id}}"></common-action-button>
|
|
51
|
+
<common-action-button action="delete" id="{{id}}"></common-action-button>
|
|
52
|
+
</div>
|
|
53
|
+
</td>
|
|
54
|
+
</tr>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
|
|
58
|
+
<!-- Empty State -->
|
|
59
|
+
<div class="flex flex-col items-center justify-center py-20 text-gray-400 {{menuList.length === 0 ? '' : 'hidden'}}">
|
|
60
|
+
<div class="w-16 h-16 bg-gray-50 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
|
61
|
+
<svg class="w-8 h-8 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path></svg>
|
|
62
|
+
</div>
|
|
63
|
+
<span class="text-sm font-medium">暂无菜单数据</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Pagination Component -->
|
|
68
|
+
<common-pagination
|
|
69
|
+
current="{{currentPage}}"
|
|
70
|
+
total="{{totalItems}}"
|
|
71
|
+
psize="{{pageSize}}"
|
|
72
|
+
onchange="{{onPageChange}}">
|
|
73
|
+
</common-pagination>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Add/Edit Modal -->
|
|
77
|
+
<common-modal title="{{isEditMode ? '编辑菜单' : '新增菜单'}}" visible="{{modalVisible}}" onclose="{{onCloseModal}}" onsave="{{onSave}}">
|
|
78
|
+
<div class="grid grid-cols-2 gap-5">
|
|
79
|
+
<div class="col-span-2">
|
|
80
|
+
<common-input label="菜单名称" obj="{{currentMenu}}" prop="menuName" placeholder="请输入菜单名称"></common-input>
|
|
81
|
+
</div>
|
|
82
|
+
<common-input label="访问路径" obj="{{currentMenu}}" prop="path" placeholder="请输入路径 (如: /system/menu)"></common-input>
|
|
83
|
+
<common-input label="排序" type="number" obj="{{currentMenu}}" prop="sort" placeholder="显示排序"></common-input>
|
|
84
|
+
<div class="col-span-2">
|
|
85
|
+
<common-input label="图标 (SVG Path)" type="textarea" obj="{{currentMenu}}" prop="icon" placeholder="请输入图标的 SVG Path"></common-input>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</common-modal>
|
|
89
|
+
</div>
|
|
90
|
+
`
|
|
91
|
+
|
|
92
|
+
export default class extends HTMLElement {
|
|
93
|
+
// Page State
|
|
94
|
+
menuList = []
|
|
95
|
+
modalVisible = false
|
|
96
|
+
isEditMode = false
|
|
97
|
+
currentMenu = { id: '', menuName: '', path: '', icon: '', sort: 0, status: 'active' }
|
|
98
|
+
searchQuery = ''
|
|
99
|
+
ths = [
|
|
100
|
+
'ID',
|
|
101
|
+
'菜单名称',
|
|
102
|
+
'路径',
|
|
103
|
+
'状态',
|
|
104
|
+
'排序',
|
|
105
|
+
'创建时间',
|
|
106
|
+
{ title: '操作', classStr: 'text-right' }
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
// Pagination State
|
|
110
|
+
currentPage = 1
|
|
111
|
+
pageSize = 10
|
|
112
|
+
totalItems = 0
|
|
113
|
+
|
|
114
|
+
constructor() {
|
|
115
|
+
super()
|
|
116
|
+
this.loadData()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async loadData() {
|
|
120
|
+
const { list, total } = await getMenusByPage(this.currentPage, this.pageSize, { key: this.searchQuery })
|
|
121
|
+
this.menuList = list
|
|
122
|
+
this.totalItems = total
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onSearch(e) {
|
|
126
|
+
this.searchQuery = e.detail
|
|
127
|
+
this.currentPage = 1
|
|
128
|
+
this.loadData()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onAdd() {
|
|
132
|
+
this.isEditMode = false
|
|
133
|
+
this.currentMenu = { id: '', menuName: '', path: '', icon: '', sort: 0, status: 'active' }
|
|
134
|
+
this.modalVisible = true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onPageChange(e) {
|
|
138
|
+
this.currentPage = e.detail
|
|
139
|
+
this.loadData()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
onCloseModal() {
|
|
143
|
+
this.modalVisible = false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async onSave() {
|
|
147
|
+
try {
|
|
148
|
+
await saveMenu(this.currentMenu)
|
|
149
|
+
this.modalVisible = false
|
|
150
|
+
this.loadData()
|
|
151
|
+
} catch (err) {
|
|
152
|
+
alert(err)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async dispatchAction(e) {
|
|
157
|
+
const btn = e.target.closest('[data-action]')
|
|
158
|
+
if (!btn) return
|
|
159
|
+
|
|
160
|
+
const { action, id } = btn.dataset
|
|
161
|
+
|
|
162
|
+
if (action === 'edit') {
|
|
163
|
+
const menu = this.menuList.find(m => m.id == id)
|
|
164
|
+
if (menu) {
|
|
165
|
+
this.isEditMode = true
|
|
166
|
+
this.currentMenu = { ...menu }
|
|
167
|
+
this.modalVisible = true
|
|
168
|
+
}
|
|
169
|
+
} else if (action === 'delete') {
|
|
170
|
+
if (confirm('确定要删除该菜单吗?')) {
|
|
171
|
+
await deleteMenu(id)
|
|
172
|
+
this.loadData()
|
|
173
|
+
}
|
|
174
|
+
} else if (action === 'toggle') {
|
|
175
|
+
await toggleMenuStatus(id)
|
|
176
|
+
this.loadData()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { getRolesByPage, deleteRole, saveRole, toggleRoleStatus } from "../api/mock.mjs"
|
|
2
|
+
|
|
3
|
+
export const innerHTML = `
|
|
4
|
+
<style>
|
|
5
|
+
/* Scoped styles if needed, but we rely mostly on Tailwind */
|
|
6
|
+
.modal-enter { opacity: 0; transform: scale(0.95); }
|
|
7
|
+
.modal-create { opacity: 1; transform: scale(1); }
|
|
8
|
+
</style>
|
|
9
|
+
<div class="h-full flex flex-col p-6 gap-6">
|
|
10
|
+
<!-- Header -->
|
|
11
|
+
<common-header title="角色管理" description="管理系统的角色与权限分配">
|
|
12
|
+
<common-toolbar button-text="新增角色" placeholder="搜索角色..." onadd="{{onAdd}}" onsearch="{{onSearch}}"></common-toolbar>
|
|
13
|
+
</common-header>
|
|
14
|
+
<!-- Table Card -->
|
|
15
|
+
<div class="flex-1 bg-white/60 dark:bg-gray-800/60 backdrop-blur-2xl rounded-2xl shadow-lg shadow-gray-200/20 dark:shadow-black/20 border border-white/40 dark:border-white/10 overflow-hidden flex flex-col relative">
|
|
16
|
+
<div class="overflow-x-auto flex-1 custom-scrollbar">
|
|
17
|
+
<table class="w-full text-left border-collapse">
|
|
18
|
+
<thead class="bg-gray-50/50 dark:bg-gray-700/30 sticky top-0 z-10 backdrop-blur-md border-b border-white/20 dark:border-white/5">
|
|
19
|
+
<tr>
|
|
20
|
+
<th each="{{ths}}" class="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider {{typeof scope === 'string' ? '' : classStr}}">
|
|
21
|
+
{{typeof scope === 'string' ? scope : title}}
|
|
22
|
+
</th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody onclick="{{dispatchAction}}" class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
26
|
+
<tr each="{{roles}}" class="hover:bg-white/40 dark:hover:bg-white/5 transition-colors group border-b border-transparent hover:border-white/20">
|
|
27
|
+
<td class="px-6 py-4 text-sm text-gray-500 font-mono">{{id}}</td>
|
|
28
|
+
<td class="px-6 py-4">
|
|
29
|
+
<div class="flex items-center gap-3">
|
|
30
|
+
<div class="w-9 h-9 rounded-full bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 flex items-center justify-center text-purple-600 dark:text-purple-400 font-bold text-sm shadow-sm">
|
|
31
|
+
{{roleName.charAt(0).toUpperCase()}}
|
|
32
|
+
</div>
|
|
33
|
+
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{roleName}}</span>
|
|
34
|
+
</div>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">{{description}}</td>
|
|
37
|
+
<td class="px-6 py-4">
|
|
38
|
+
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium {{status === 'active' ? 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400' : 'bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400'}}">
|
|
39
|
+
<span class="w-1.5 h-1.5 rounded-full {{status === 'active' ? 'bg-green-500' : 'bg-red-500'}}"></span>
|
|
40
|
+
{{status === 'active' ? '正常' : '禁用'}}
|
|
41
|
+
</span>
|
|
42
|
+
</td>
|
|
43
|
+
<td class="px-6 py-4 text-sm text-gray-500">{{createdAt}}</td>
|
|
44
|
+
<td class="px-6 py-4 text-right">
|
|
45
|
+
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
46
|
+
<common-action-button action="toggle" id="{{id}}" title="{{status === 'active' ? '禁用' : '启用'}}"></common-action-button>
|
|
47
|
+
<common-action-button action="edit" id="{{id}}"></common-action-button>
|
|
48
|
+
<common-action-button action="delete" id="{{id}}"></common-action-button>
|
|
49
|
+
</div>
|
|
50
|
+
</td>
|
|
51
|
+
</tr>
|
|
52
|
+
</tbody>
|
|
53
|
+
</table>
|
|
54
|
+
|
|
55
|
+
<!-- Empty State -->
|
|
56
|
+
<div class="flex flex-col items-center justify-center py-20 text-gray-400 {{roles.length === 0 ? '' : 'hidden'}}">
|
|
57
|
+
<div class="w-16 h-16 bg-gray-50 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
|
58
|
+
<svg class="w-8 h-8 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
|
59
|
+
</div>
|
|
60
|
+
<span class="text-sm font-medium">暂无角色数据</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Pagination -->
|
|
65
|
+
<common-pagination current-page="{{currentPage}}" page-size="{{pageSize}}" total="{{total}}" onchange="{{onPageChange}}"></common-pagination>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Modal Component -->
|
|
69
|
+
<common-modal title="{{isEditMode ? '编辑角色' : '新增角色'}}" visible="{{modalVisible}}" onclose="{{onCloseModal}}" onsave="{{onSave}}">
|
|
70
|
+
<div class="space-y-5">
|
|
71
|
+
<common-input label="角色名称" obj="{{currentRole}}" prop="roleName" placeholder="请输入角色名称"></common-input>
|
|
72
|
+
<common-input label="角色描述" type="textarea" obj="{{currentRole}}" prop="description" placeholder="请输入角色描述"></common-input>
|
|
73
|
+
</div>
|
|
74
|
+
</common-modal>
|
|
75
|
+
</div>
|
|
76
|
+
`
|
|
77
|
+
|
|
78
|
+
export default class extends HTMLElement {
|
|
79
|
+
|
|
80
|
+
roles = []
|
|
81
|
+
modalVisible = false
|
|
82
|
+
isEditMode = false
|
|
83
|
+
currentRole = { id: '', roleName: '', description: '', status: 'active' }
|
|
84
|
+
searchQuery = ''
|
|
85
|
+
ths = [
|
|
86
|
+
'ID',
|
|
87
|
+
'角色名称',
|
|
88
|
+
'描述',
|
|
89
|
+
'状态',
|
|
90
|
+
'创建时间',
|
|
91
|
+
{
|
|
92
|
+
title: '操作',
|
|
93
|
+
classStr: 'text-right'
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
// Pagination State
|
|
98
|
+
currentPage = 1
|
|
99
|
+
pageSize = 10
|
|
100
|
+
total = 0
|
|
101
|
+
|
|
102
|
+
constructor() {
|
|
103
|
+
super()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ready() {
|
|
107
|
+
this.loadData()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async loadData() {
|
|
111
|
+
await this.fetchRoles()
|
|
112
|
+
console.log('Role data loaded')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async fetchRoles() {
|
|
116
|
+
try {
|
|
117
|
+
const res = await getRolesByPage(this.currentPage, this.pageSize, {
|
|
118
|
+
key: this.searchQuery
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
this.roles = res.list
|
|
122
|
+
this.total = res.total
|
|
123
|
+
|
|
124
|
+
// Ensure current page is valid after search/filter
|
|
125
|
+
const totalPage = Math.ceil(this.total / this.pageSize) || 1
|
|
126
|
+
if (this.currentPage > totalPage) {
|
|
127
|
+
this.currentPage = 1
|
|
128
|
+
return this.fetchRoles()
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Failed to fetch roles:', error)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async onPageChange(page) {
|
|
136
|
+
this.currentPage = page
|
|
137
|
+
await this.fetchRoles()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async onSearch(e) {
|
|
141
|
+
this.searchQuery = (e.detail?.value ?? e.target.value).trim()
|
|
142
|
+
this.currentPage = 1
|
|
143
|
+
await this.fetchRoles()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onAdd() {
|
|
147
|
+
this.isEditMode = false
|
|
148
|
+
Object.assign(this.currentRole, { id: '', roleName: '', description: '', status: 'active' })
|
|
149
|
+
this.modalVisible = true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
onCloseModal() {
|
|
153
|
+
this.modalVisible = false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async dispatchAction(e) {
|
|
157
|
+
const btn = e.target.closest('button[data-action]')
|
|
158
|
+
if (!btn) return
|
|
159
|
+
|
|
160
|
+
const action = btn.dataset.action
|
|
161
|
+
const id = parseInt(btn.dataset.id)
|
|
162
|
+
|
|
163
|
+
if (action === 'delete') {
|
|
164
|
+
if (confirm(`确定要删除角色 ID:${id} 吗?`)) {
|
|
165
|
+
await deleteRole(id)
|
|
166
|
+
await this.fetchRoles()
|
|
167
|
+
}
|
|
168
|
+
} else if (action === 'toggle') {
|
|
169
|
+
await toggleRoleStatus(id)
|
|
170
|
+
await this.fetchRoles()
|
|
171
|
+
} else if (action === 'edit') {
|
|
172
|
+
const role = this.roles.find(r => r.id === id)
|
|
173
|
+
if (role) this.prepareEdit(role)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
prepareEdit(role) {
|
|
178
|
+
this.isEditMode = true
|
|
179
|
+
Object.assign(this.currentRole, role)
|
|
180
|
+
this.modalVisible = true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async onSave() {
|
|
184
|
+
console.log(this.currentRole)
|
|
185
|
+
if (!this.currentRole.roleName) {
|
|
186
|
+
alert('角色名称不能为空')
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await saveRole(this.currentRole)
|
|
192
|
+
await this.fetchRoles()
|
|
193
|
+
this.modalVisible = false
|
|
194
|
+
} catch (error) {
|
|
195
|
+
alert('保存失败: ' + error)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { getUsersByPage, deleteUser, saveUser, toggleUserStatus } from "../api/mock.mjs"
|
|
2
|
+
|
|
3
|
+
export const innerHTML = `
|
|
4
|
+
<style>
|
|
5
|
+
/* Scoped styles if needed, but we rely mostly on Tailwind */
|
|
6
|
+
.modal-enter { opacity: 0; transform: scale(0.95); }
|
|
7
|
+
.modal-create { opacity: 1; transform: scale(1); }
|
|
8
|
+
</style>
|
|
9
|
+
<div class="h-full flex flex-col p-6 gap-6">
|
|
10
|
+
<!-- Header -->
|
|
11
|
+
<common-header title="用户管理" description="管理系统用户的账号与权限">
|
|
12
|
+
<common-toolbar button-text="新增用户" placeholder="搜索用户..." onadd="{{onAdd}}" onsearch="{{onSearch}}"></common-toolbar>
|
|
13
|
+
</common-header>
|
|
14
|
+
|
|
15
|
+
<!-- Table Card -->
|
|
16
|
+
<div class="flex-1 bg-white/60 dark:bg-gray-800/60 backdrop-blur-2xl rounded-2xl shadow-lg shadow-gray-200/20 dark:shadow-black/20 border border-white/40 dark:border-white/10 overflow-hidden flex flex-col relative">
|
|
17
|
+
<div class="overflow-x-auto flex-1 custom-scrollbar">
|
|
18
|
+
<table class="w-full text-left border-collapse">
|
|
19
|
+
<thead class="bg-gray-50/50 dark:bg-gray-700/30 sticky top-0 z-10 backdrop-blur-md border-b border-white/20 dark:border-white/5">
|
|
20
|
+
<tr>
|
|
21
|
+
<th each="{{ths}}" class="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider {{typeof scope === 'string' ? '' : classStr}}">
|
|
22
|
+
{{typeof scope === 'string' ? scope : title}}
|
|
23
|
+
</th>
|
|
24
|
+
</tr>
|
|
25
|
+
</thead>
|
|
26
|
+
<tbody onclick="{{dispatchAction}}" class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
27
|
+
<tr each="{{users}}" class="hover:bg-white/40 dark:hover:bg-white/5 transition-colors group border-b border-transparent hover:border-white/20">
|
|
28
|
+
<td class="px-6 py-4 text-sm text-gray-500 font-mono">{{id}}</td>
|
|
29
|
+
<td class="px-6 py-4">
|
|
30
|
+
<div class="flex items-center gap-3">
|
|
31
|
+
<div class="w-9 h-9 rounded-full bg-gradient-to-br from-blue-100 to-indigo-100 dark:from-blue-900/30 dark:to-indigo-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold text-sm shadow-sm">
|
|
32
|
+
{{username.charAt(0).toUpperCase()}}
|
|
33
|
+
</div>
|
|
34
|
+
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{username}}</span>
|
|
35
|
+
</div>
|
|
36
|
+
</td>
|
|
37
|
+
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">{{name}}</td>
|
|
38
|
+
<td class="px-6 py-4">
|
|
39
|
+
<!-- Helper logic needed for conditional class, simplified here -->
|
|
40
|
+
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium {{status === 'active' ? 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400' : 'bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400'}}">
|
|
41
|
+
<span class="w-1.5 h-1.5 rounded-full {{status === 'active' ? 'bg-green-500' : 'bg-red-500'}}"></span>
|
|
42
|
+
{{status === 'active' ? '正常' : '禁用'}}
|
|
43
|
+
</span>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="px-6 py-4 text-sm text-gray-500">{{createdAt}}</td>
|
|
46
|
+
<td class="px-6 py-4 text-right">
|
|
47
|
+
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
48
|
+
<common-action-button action="toggle" id="{{id}}" title="{{status === 'active' ? '禁用' : '启用'}}"></common-action-button>
|
|
49
|
+
<common-action-button action="edit" id="{{id}}"></common-action-button>
|
|
50
|
+
<common-action-button action="delete" id="{{id}}"></common-action-button>
|
|
51
|
+
</div>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
|
|
57
|
+
<!-- Empty State -->
|
|
58
|
+
<div class="flex flex-col items-center justify-center py-20 text-gray-400 {{users.length === 0 ? '' : 'hidden'}}">
|
|
59
|
+
<div class="w-16 h-16 bg-gray-50 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
|
60
|
+
<svg class="w-8 h-8 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path></svg>
|
|
61
|
+
</div>
|
|
62
|
+
<span class="text-sm font-medium">暂无用户数据</span>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Pagination -->
|
|
67
|
+
<common-pagination current-page="{{currentPage}}" page-size="{{pageSize}}" total="{{total}}" onchange="{{onPageChange}}"></common-pagination>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Modal Component -->
|
|
71
|
+
<common-modal title="{{isEditMode ? '编辑用户' : '新增用户'}}" visible="{{modalVisible}}" onclose="{{onCloseModal}}" onsave="{{onSave}}">
|
|
72
|
+
<div class="space-y-5">
|
|
73
|
+
<common-input label="用户名" obj="{{currentUser}}" prop="username" placeholder="请输入用户名"></common-input>
|
|
74
|
+
<common-input label="姓名" obj="{{currentUser}}" prop="name" placeholder="请输入姓名"></common-input>
|
|
75
|
+
<common-input label="密码" type="password" obj="{{currentUser}}" prop="password" placeholder="请输入密码" class="{{isEditMode ? 'hidden' : ''}}"></common-input>
|
|
76
|
+
</div>
|
|
77
|
+
</common-modal>
|
|
78
|
+
</div>
|
|
79
|
+
`
|
|
80
|
+
|
|
81
|
+
export default class extends HTMLElement {
|
|
82
|
+
|
|
83
|
+
username = ''
|
|
84
|
+
users = []
|
|
85
|
+
modalVisible = false
|
|
86
|
+
isEditMode = false
|
|
87
|
+
currentUser = { id: '', username: '', name: '', password: '', status: 'active' }
|
|
88
|
+
searchQuery = ''
|
|
89
|
+
ths = [
|
|
90
|
+
'ID',
|
|
91
|
+
'用户',
|
|
92
|
+
'姓名',
|
|
93
|
+
'状态',
|
|
94
|
+
'创建时间',
|
|
95
|
+
{
|
|
96
|
+
title: '操作',
|
|
97
|
+
classStr: 'text-right'
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
// Pagination State
|
|
102
|
+
currentPage = 1
|
|
103
|
+
pageSize = 10
|
|
104
|
+
total = 0
|
|
105
|
+
|
|
106
|
+
ready() {
|
|
107
|
+
this.loadData()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async loadData() {
|
|
111
|
+
await this.fetchUsers()
|
|
112
|
+
console.log('Data loaded')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async fetchUsers() {
|
|
116
|
+
try {
|
|
117
|
+
const res = await getUsersByPage(this.currentPage, this.pageSize, {
|
|
118
|
+
key: this.searchQuery
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
this.users = res.list
|
|
122
|
+
this.total = res.total
|
|
123
|
+
// Ensure current page is valid after search/filter
|
|
124
|
+
const totalPage = Math.ceil(this.total / this.pageSize) || 1
|
|
125
|
+
if (this.currentPage > totalPage) {
|
|
126
|
+
this.currentPage = 1
|
|
127
|
+
return this.fetchUsers()
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('Failed to fetch users:', error)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async onPageChange(page) {
|
|
135
|
+
this.currentPage = page
|
|
136
|
+
await this.fetchUsers()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async onSearch(e) {
|
|
140
|
+
this.searchQuery = (e.detail?.value ?? e.target.value).trim()
|
|
141
|
+
this.currentPage = 1 // Reset to first page on search
|
|
142
|
+
await this.fetchUsers()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onAdd() {
|
|
146
|
+
this.isEditMode = false
|
|
147
|
+
Object.assign(this.currentUser, { id: '', username: '', name: '', password: '', status: 'active' })
|
|
148
|
+
this.modalVisible = true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
onCloseModal() {
|
|
152
|
+
this.modalVisible = false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async dispatchAction(e) {
|
|
156
|
+
// Find the button (could be clicked on svg or path)
|
|
157
|
+
const btn = e.target.closest('button[data-action]')
|
|
158
|
+
if (!btn) return
|
|
159
|
+
|
|
160
|
+
const action = btn.dataset.action
|
|
161
|
+
const id = parseInt(btn.dataset.id)
|
|
162
|
+
|
|
163
|
+
if (action === 'delete') {
|
|
164
|
+
if (confirm(`确定要删除用户 ID:${id} 吗?`)) {
|
|
165
|
+
await deleteUser(id)
|
|
166
|
+
await this.fetchUsers()
|
|
167
|
+
}
|
|
168
|
+
} else if (action === 'toggle') {
|
|
169
|
+
await toggleUserStatus(id)
|
|
170
|
+
await this.fetchUsers()
|
|
171
|
+
} else if (action === 'edit') {
|
|
172
|
+
const user = this.users.find(u => u.id === id)
|
|
173
|
+
if (user) this.prepareEdit(user)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
prepareEdit(user) {
|
|
178
|
+
this.isEditMode = true
|
|
179
|
+
// Clone to avoid direct mutation of list item until saved
|
|
180
|
+
Object.assign(this.currentUser, user, { password: '' })
|
|
181
|
+
this.modalVisible = true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async onSave() {
|
|
185
|
+
console.log(123123, this.currentUser)
|
|
186
|
+
if (!this.currentUser.username || !this.currentUser.name) {
|
|
187
|
+
alert('用户名和姓名不能为空')
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await saveUser(this.currentUser)
|
|
193
|
+
await this.fetchUsers()
|
|
194
|
+
this.modalVisible = false
|
|
195
|
+
} catch (error) {
|
|
196
|
+
alert('保存失败: ' + error)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|