update crud and env.js

v3
veypi 6 months ago
parent ae463e0723
commit e83f2da265

@ -21,5 +21,5 @@ func init() {
crud.All(appRouter.SubRouter("resource"), cfg.DB, models.Resource{})
crud.All(appRouter.SubRouter("user"), cfg.DB, models.AppUser{})
crud.All(appRouter.SubRouter("role"), cfg.DB, models.Role{})
crud.All(Router.SubRouter("access"), cfg.DB, models.Access{})
crud.All(appRouter.SubRouter("access"), cfg.DB, models.Access{})
}

@ -8,8 +8,6 @@
package api
import (
"fmt"
"github.com/veypi/OneAuth/api/app"
"github.com/veypi/OneAuth/api/token"
"github.com/veypi/OneAuth/api/user"
@ -26,5 +24,5 @@ var (
)
var _ = Router.Any("*", func(x *vigo.X) error {
return fmt.Errorf("404")
return vigo.ErrNotFound
})

@ -8,12 +8,15 @@
package user
import (
"github.com/veypi/OneAuth/api/user/role"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes/vigo"
"github.com/vyes/vigo/contrib/crud"
)
var Router = vigo.NewRouter()
var (
_ = Router.Extend("/:user_id/user_role", role.Router)
)
func init() {
crud.All(Router.SubRouter("/:user_id/user_role"), cfg.DB, models.UserRole{})
}

@ -5,6 +5,7 @@ import (
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/models"
"github.com/vyes/vigo"
"github.com/vyes/vigo/contrib/crud"
)
var _ = Router.Delete("/:user_id", auth.Check("user", "user_id", auth.DoDelete), userDelete)
@ -34,7 +35,7 @@ func userGet(x *vigo.X) (any, error) {
return data, err
}
var _ = Router.Get("/", "list user", listOpts{}, auth.Check("user", "", auth.DoRead), userList)
var _ = Router.Get("/", "list user", listOpts{}, auth.Check("user", "", auth.DoUpdate), crud.List(cfg.DB, &models.User{}))
type listOpts struct {
Username *string `json:"username" parse:"query"`

@ -69,10 +69,10 @@ func (m *AppUser) AfterUpdate(tx *gorm.DB) error {
type Resource struct {
BaseModel
AppID string `json:"app_id" methods:"*list,post" parse:"path"`
AppID string `json:"app_id" parse:"path@app_id"`
App *App `json:"-" gorm:"foreignKey:AppID;references:ID"`
Name string `json:"name" methods:"post" parse:"json"`
Des string `json:"des" methods:"post,*patch" parse:"json"`
Name string `json:"name" parse:"json"`
Des string `json:"des" parse:"json"`
}
type Role struct {
@ -87,20 +87,19 @@ type Role struct {
type Access struct {
BaseModel
AppID string `json:"app_id" gorm:"index;type:varchar(32)" methods:"post,list" parse:"json"`
AppID string `json:"app_id" gorm:"index;type:varchar(32)" parse:"path@app_id"`
App *App `json:"-" gorm:"foreignKey:AppID;references:ID"`
UserID *string `json:"user_id" gorm:"index;type:varchar(32);default: null" methods:"post,list" parse:"json"`
UserID *string `json:"user_id" gorm:"index;type:varchar(32);default: null" parse:"json"`
User *User `json:"-" gorm:"foreignKey:UserID;references:ID"`
RoleID *string `json:"role_id" gorm:"index;type:varchar(32);default: null" methods:"post,list" parse:"json"`
RoleID *string `json:"role_id" gorm:"index;type:varchar(32);default: null" parse:"json"`
Role *Role `json:"-" gorm:"foreignKey:RoleID;references:ID"`
ResourceID *string `json:"resource_id" gorm:"index;type:varchar(32);default: null" methods:"post,list" parse:"json"`
ResourceID *string `json:"resource_id" gorm:"index;type:varchar(32);default: null" parse:"json"`
Resource *Resource `json:"-" gorm:"foreignKey:ResourceID;references:ID"`
Name string `json:"name" methods:"post,*list" parse:"json"`
TID string `json:"tid" methods:"post,*patch" parse:"json"`
Level uint `json:"level" methods:"post,*patch" parse:"json"`
Name string `json:"name" parse:"json"`
TID string `json:"tid" parse:"json"`
Level uint `json:"level" parse:"json"`
}

@ -24,16 +24,16 @@ type User struct {
type UserRole struct {
BaseModel
UserID string `json:"user_id" methods:"*list,post" parse:"path"`
UserID string `json:"user_id" parse:"path@user_id"`
User *User `json:"-" gorm:"foreignKey:UserID;references:ID"`
RoleID string `json:"role_id" methods:"*list,post" parse:"json"`
RoleID string `json:"role_id" parse:"json"`
Role *Role `json:"-" gorm:"foreignKey:RoleID;references:ID"`
AppID string `json:"app_id" methods:"*list,post" parse:"json"`
AppID string `json:"app_id" parse:"json"`
App *App `json:"-" gorm:"foreignKey:AppID;references:ID"`
Status string `json:"status" methods:"post,*patch,*list, *go@get@/go" parse:"json@ssss"`
Status string `json:"status"`
}
func (m *UserRole) AfterCreate(tx *gorm.DB) error {

@ -167,16 +167,6 @@
line-height: 1.6;
}
a {
color: var(--color-link);
text-decoration: none;
transition: var(--transition);
}
a:hover {
color: var(--color-link-hover);
text-decoration: underline;
}
hr {
border: 0;
@ -278,9 +268,7 @@
input[type="number"],
textarea {
border: 1px solid var(--color-border);
padding: 8px 12px;
border-radius: 4px;
background-color: white;
color: var(--text-color);
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}

@ -1,33 +1,10 @@
<!DOCTYPE html>
<html>
<style>
.body {}
.menu-item {
padding: 10px 20px;
cursor: pointer;
transition: background 0.3s;
}
</style>
<body>
<div class="menu-item">
<a :href="`/app/main?id=${id}`">应用简介</a>
</div>
<div class="menu-item">
<a :href="`/app/user?id=${id}`">用户管理</a>
</div>
<div class="menu-item">
<a :href="`/app/auth?id=${id}`">权限管理</a>
</div>
<div class="menu-item">
<a :href="`/app/settings?id=${id}`">应用设置</a>
</div>
</body>
<script setup>
const params = new URLSearchParams(window.location.search)
id = params.get('id')
id = $router.params.id
</script>
</html>

@ -1,630 +1,260 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>多字段表格管理界面</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@font-face {
font-family: 'LucideIcons';
src: url(https://cdn.jsdelivr.net/npm/lucide-static@latest/font/Lucide.ttf) format('truetype');
}
.lucide {
font-family: 'LucideIcons';
font-style: normal;
font-weight: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
background-color: #f8fafc;
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s ease-in-out;
}
.modal-content {
transition: transform 0.3s ease-in-out;
}
.hidden {
display: none;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
body {}
.table-column {
flex-grow: 1;
max-width: 30%;
border-bottom: 2px solid black;
background-color: var(--color-background, #fff);
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
.sticky-column {
position: sticky;
z-index: 10;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
.header-key {
border-bottom: 2px solid black;
padding: 0 0.5rem;
font-size: 1.2rem;
height: 3rem;
line-height: 3rem;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
box-sizing: border-box;
}
</style>
</head>
<body class="p-6 md:p-10">
<div class="max-w-7xl mx-auto bg-white p-6 rounded-lg shadow-lg">
<h1 class="text-2xl font-semibold text-gray-800 mb-6">客户信息管理</h1>
<div class="flex flex-col md:flex-row justify-between items-center mb-6 gap-4">
<div class="relative w-full md:w-1/3">
<input
type="text"
placeholder="搜索姓名或记录编号..."
!value="searchQuery"
@input="searchQuery = $event.target.value"
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<span class="lucide absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">search</span>
</div>
<button
@click="openModal('addEditModal', 'add')"
class="w-full md:w-auto bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-lg flex items-center justify-center gap-2 transition duration-150 ease-in-out"
>
<span class="lucide">plus</span> 新增记录
</button>
</div>
.table-value {
display: block;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0.5rem;
height: 3rem;
line-height: 2rem;
}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
记录编号
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
姓名
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
状态
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
创建日期
</th>
<th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="(record, index) in filteredRecords" :key="index">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ record.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{{ record.name }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
:class="{
'bg-green-100 text-green-800': record.status === 'active',
'bg-yellow-100 text-yellow-800': record.status === 'pending',
'bg-red-100 text-red-800': record.status === 'inactive',
}"
>
{{ record.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{{ record.createdAt }}</td>
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium space-x-2">
<button
@click="openModal('viewModal', record)"
title="查看"
class="text-blue-600 hover:text-blue-800 transition duration-150 ease-in-out"
>
<span class="lucide">eye</span>
</button>
<button
@click="openModal('addEditModal', 'edit', record)"
title="编辑"
class="text-yellow-600 hover:text-yellow-800 transition duration-150 ease-in-out"
>
<span class="lucide">pencil</span>
</button>
<button
@click="openModal('deleteConfirmModal', record)"
title="删除"
class="text-red-600 hover:text-red-800 transition duration-150 ease-in-out"
>
<span class="lucide">trash-2</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
.table-value[odd='1'] {
background-color: color-mix(in srgb, var(--color-background, #fff), #888 10%);
}
<div class="mt-6 flex justify-center">
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<a href="#" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span class="sr-only">Previous</span>
<span class="lucide h-5 w-5">chevron-left</span>
</a>
<a href="#" aria-current="page" class="z-10 bg-blue-50 border-blue-500 text-blue-600 relative inline-flex items-center px-4 py-2 border text-sm font-medium">
1
</a>
<a href="#" class="bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium">
2
</a>
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
...
</span>
<a href="#" class="bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium">
10
</a>
<a href="#" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span class="sr-only">Next</span>
<span class="lucide h-5 w-5">chevron-right</span>
</a>
</nav>
</div>
</div>
.table-btn {}
<!-- 查看弹窗 -->
<div id="viewModal" class="fixed inset-0 z-50 hidden items-center justify-center p-4 modal-backdrop">
<div class="bg-white rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col modal-content transform scale-95">
<div class="flex justify-between items-center p-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">记录详情 ({{ currentRecord.id }})</h2>
<button @click="closeModal('viewModal')" class="text-gray-400 hover:text-gray-600">
<span class="lucide text-2xl">x</span>
</button>
</div>
<div class="p-6 overflow-y-auto">
<!-- 显示当前记录详细信息 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<fieldset class="border border-gray-300 p-4 rounded-md md:col-span-2">
<legend class="text-lg font-medium text-gray-700 px-2">基本信息</legend>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-2">
<div><strong class="text-gray-600">ID:</strong> <span class="text-gray-800">{{ currentRecord.id }}</span></div>
<div><strong class="text-gray-600">记录编号:</strong> <span class="text-gray-800">{{ currentRecord.recordNumber }}</span></div>
<div><strong class="text-gray-600">状态:</strong> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full" :class="statusClass(currentRecord.status)">{{ currentRecord.status }}</span></div>
<div><strong class="text-gray-600">创建日期:</strong> <span class="text-gray-800">{{ currentRecord.createdAt }}</span></div>
<div><strong class="text-gray-600">创建人:</strong> <span class="text-gray-800">{{ currentRecord.createdBy }}</span></div>
<div><strong class="text-gray-600">最后修改日期:</strong> <span class="text-gray-800">{{ currentRecord.lastModifiedAt }}</span></div>
<div><strong class="text-gray-600">修改人:</strong> <span class="text-gray-800">{{ currentRecord.lastModifiedBy }}</span></div>
<div><strong class="text-gray-600">类别:</strong> <span class="text-gray-800">{{ currentRecord.category }}</span></div>
<div><strong class="text-gray-600">类型:</strong> <span class="text-gray-800">{{ currentRecord.type }}</span></div>
<div><strong class="text-gray-600">优先级:</strong> <span class="text-gray-800">{{ currentRecord.priority }}</span></div>
</div>
</fieldset>
.dialog {
min-height: 50vh;
max-height: 80vh;
overflow: auto;
width: 50vw;
background-color: var(--color-background, #fff);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 2rem;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
}
<fieldset class="border border-gray-300 p-4 rounded-md md:col-span-2">
<legend class="text-lg font-medium text-gray-700 px-2">联系信息</legend>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-2">
<div><strong class="text-gray-600">姓名:</strong> <span class="text-gray-800">{{ currentRecord.fullName }}</span></div>
<div><strong class="text-gray-600">邮箱:</strong> <span class="text-gray-800">{{ currentRecord.email }}</span></div>
<div><strong class="text-gray-600">电话:</strong> <span class="text-gray-800">{{ currentRecord.phone }}</span></div>
<div class="sm:col-span-2"><strong class="text-gray-600">地址1:</strong> <span class="text-gray-800">{{ currentRecord.address1 }}</span></div>
<div><strong class="text-gray-600">地址2:</strong> <span class="text-gray-800">{{ currentRecord.address2 }}</span></div>
<div><strong class="text-gray-600">城市:</strong> <span class="text-gray-800">{{ currentRecord.city }}</span></div>
<div><strong class="text-gray-600">省份:</strong> <span class="text-gray-800">{{ currentRecord.province }}</span></div>
<div><strong class="text-gray-600">邮编:</strong> <span class="text-gray-800">{{ currentRecord.postalCode }}</span></div>
<div><strong class="text-gray-600">国家:</strong> <span class="text-gray-800">{{ currentRecord.country }}</span></div>
</div>
</fieldset>
<fieldset class="border border-gray-300 p-4 rounded-md md:col-span-2">
<legend class="text-lg font-medium text-gray-700 px-2">其他信息</legend>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-2">
<div><strong class="text-gray-600">开始日期:</strong> <span class="text-gray-800">{{ currentRecord.startDate }}</span></div>
<div><strong class="text-gray-600">结束日期:</strong> <span class="text-gray-800">{{ currentRecord.endDate }}</span></div>
<div><strong class="text-gray-600">金额:</strong> <span class="text-gray-800">{{ currentRecord.amount }}</span></div>
<div><strong class="text-gray-600">数量:</strong> <span class="text-gray-800">{{ currentRecord.quantity }}</span></div>
<div class="sm:col-span-2 md:col-span-3"><strong class="text-gray-600">描述:</strong> <p class="text-gray-800 mt-1">{{ currentRecord.description }}</p></div>
<div class="sm:col-span-2 md:col-span-3"><strong class="text-gray-600">备注:</strong> <p class="text-gray-800 mt-1">{{ currentRecord.notes }}</p></div>
</div>
</fieldset>
</div>
</div>
<div class="flex justify-end p-4 border-t border-gray-200 bg-gray-50">
<button @click="closeModal('viewModal')" class="bg-gray-500 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-150 ease-in-out">
关闭
</button>
</div>
</div>
</div>
::-webkit-scrollbar,
::-webkit-scrollbar-track {
width: 0.25rem;
height: 0.25rem;
background: none;
border-radius: 5px;
}
<!-- 新增/编辑弹窗 -->
<div id="addEditModal" class="fixed inset-0 z-50 hidden items-center justify-center p-4 modal-backdrop">
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col modal-content transform scale-95">
<div class="flex justify-between items-center p-4 border-b border-gray-200">
<h2 id="modalTitle" class="text-xl font-semibold text-gray-800">{{ isEditMode ? '编辑记录' : '新增记录' }}</h2>
<button @click="closeModal('addEditModal')" class="text-gray-400 hover:text-gray-600">
<span class="lucide text-2xl">x</span>
</button>
</div>
<form class="flex-grow overflow-y-auto p-6 space-y-6">
<fieldset class="border border-gray-300 p-4 rounded-md">
<legend class="text-lg font-medium text-gray-700 px-2">基本信息</legend>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-2">
<div>
<label for="recordNumber" class="block text-sm font-medium text-gray-700">记录编号</label>
<input
type="text"
id="recordNumber"
name="recordNumber"
!value="currentRecord.recordNumber"
readonly
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 focus:outline-none sm:text-sm"
/>
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700">状态 *</label>
<select
id="status"
name="status"
!value="currentRecord.status"
@change="currentRecord.status = $event.target.value"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="active">活动</option>
<option value="pending">待定</option>
<option value="inactive">非活动</option>
</select>
</div>
<div>
<label for="category" class="block text-sm font-medium text-gray-700">类别</label>
<input
type="text"
id="category"
name="category"
!value="currentRecord.category"
@input="currentRecord.category = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700">类型</label>
<input
type="text"
id="type"
name="type"
!value="currentRecord.type"
@input="currentRecord.type = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="priority" class="block text-sm font-medium text-gray-700">优先级</label>
<select
id="priority"
name="priority"
!value="currentRecord.priority"
@change="currentRecord.priority = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="high"></option>
<option value="medium"></option>
<option value="low"></option>
</select>
</div>
</div>
</fieldset>
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 5px;
transform: translateX(10px);
}
<fieldset class="border border-gray-300 p-4 rounded-md">
<legend class="text-lg font-medium text-gray-700 px-2">联系信息</legend>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-2">
<div>
<label for="fullName" class="block text-sm font-medium text-gray-700">姓名 *</label>
<input
type="text"
id="fullName"
name="fullName"
!value="currentRecord.fullName"
@input="currentRecord.fullName = $event.target.value"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">邮箱</label>
<input
type="email"
id="email"
name="email"
!value="currentRecord.email"
@input="currentRecord.email = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="phone" class="block text-sm font-medium text-gray-700">电话</label>
<input
type="tel"
id="phone"
name="phone"
!value="currentRecord.phone"
@input="currentRecord.phone = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div class="sm:col-span-2">
<label for="address1" class="block text-sm font-medium text-gray-700">地址1</label>
<input
type="text"
id="address1"
name="address1"
!value="currentRecord.address1"
@input="currentRecord.address1 = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="address2" class="block text-sm font-medium text-gray-700">地址2</label>
<input
type="text"
id="address2"
name="address2"
!value="currentRecord.address2"
@input="currentRecord.address2 = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="city" class="block text-sm font-medium text-gray-700">城市</label>
<input
type="text"
id="city"
name="city"
!value="currentRecord.city"
@input="currentRecord.city = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="province" class="block text-sm font-medium text-gray-700">省份</label>
<input
type="text"
id="province"
name="province"
!value="currentRecord.province"
@input="currentRecord.province = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="postalCode" class="block text-sm font-medium text-gray-700">邮编</label>
<input
type="text"
id="postalCode"
name="postalCode"
!value="currentRecord.postalCode"
@input="currentRecord.postalCode = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="country" class="block text-sm font-medium text-gray-700">国家</label>
<input
type="text"
id="country"
name="country"
!value="currentRecord.country"
@input="currentRecord.country = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
</div>
</fieldset>
.keysearch {
padding: 0.5rem 1rem;
font-size: 0.875rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
outline: none;
transition: border-color 0.2s ease-in-out;
}
<fieldset class="border border-gray-300 p-4 rounded-md">
<legend class="text-lg font-medium text-gray-700 px-2">其他信息</legend>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-2">
<div>
<label for="startDate" class="block text-sm font-medium text-gray-700">开始日期</label>
<input
type="date"
id="startDate"
name="startDate"
!value="currentRecord.startDate"
@input="currentRecord.startDate = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="endDate" class="block text-sm font-medium text-gray-700">结束日期</label>
<input
type="date"
id="endDate"
name="endDate"
!value="currentRecord.endDate"
@input="currentRecord.endDate = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="amount" class="block text-sm font-medium text-gray-700">金额</label>
<input
type="number"
id="amount"
name="amount"
step="0.01"
!value="currentRecord.amount"
@input="currentRecord.amount = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label for="quantity" class="block text-sm font-medium text-gray-700">数量</label>
<input
type="number"
id="quantity"
name="quantity"
!value="currentRecord.quantity"
@input="currentRecord.quantity = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div class="sm:col-span-2 md:col-span-3">
<label for="description" class="block text-sm font-medium text-gray-700">描述</label>
<textarea
id="description"
name="description"
rows="3"
!value="currentRecord.description"
@input="currentRecord.description = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
></textarea>
</div>
<div class="sm:col-span-2 md:col-span-3">
<label for="notes" class="block text-sm font-medium text-gray-700">备注</label>
<textarea
id="notes"
name="notes"
rows="3"
!value="currentRecord.notes"
@input="currentRecord.notes = $event.target.value"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
></textarea>
</div>
</div>
</fieldset>
</form>
<div class="flex justify-end p-4 border-t border-gray-200 bg-gray-50">
<button @click="closeModal('addEditModal')" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold py-2 px-4 rounded-lg mr-2 transition duration-150 ease-in-out">
取消
</button>
<button @click="saveRecord" class="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-lg transition duration-150 ease-in-out">
保存
</button>
</div>
</div>
</div>
.keysearch:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.2);
}
</style>
<!-- 删除确认弹窗 -->
<div id="deleteConfirmModal" class="fixed inset-0 z-50 hidden items-center justify-center p-4 modal-backdrop">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md modal-content transform scale-95">
<div class="flex justify-between items-center p-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-800">确认删除</h2>
<button @click="closeModal('deleteConfirmModal')" class="text-gray-400 hover:text-gray-600">
<span class="lucide text-2xl">x</span>
</button>
</div>
<div class="p-6">
<p class="text-sm text-gray-600">您确定要删除记录 "{{ currentRecord.recordNumber }}" 吗?此操作无法撤销。</p>
</div>
<div class="flex justify-end p-4 border-t border-gray-200 bg-gray-50">
<button @click="closeModal('deleteConfirmModal')" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold py-2 px-4 rounded-lg mr-2 transition duration-150 ease-in-out">
取消
</button>
<button @click="confirmDelete" class="bg-red-600 hover:bg-red-700 text-white font-semibold py-2 px-4 rounded-lg transition duration-150 ease-in-out">
确认删除
</button>
<body>
<div class="flex justify-evenly overflow-x-auto">
<div v-if='!key.hidden' class="table-column" :class="{'sticky-column':index===0}" style="min-height: 21rem;left:0"
v-for='(key,index) in keys'>
<div class="header-key">{{key.label||key.name}}</div>
<div class="table-value" :odd='index%2' v-for='(row, index) in data'>
<vslot :name='key.name' v='row,index' :style="key.style">
<div refu='input' v:value='row[key.name]' :type="key.type==='textarea'?'text':key.type"
:required='key.required' :validate='key.validate' :opts='key.opts'
v-if='editable && !key.disabled && row._enable'>
</div>
<div v-else>
{{ (key.field?key.field(row):row[key.name]) || '&nbsp;'}}
</div>
</vslot>
</div>
</div>
<div class="table-column sticky-column" style="right:0;min-width: 8rem;">
<vslot name='_key' class="header-key">
<div refu='dropdown' class="w-full">
<div refu='icon' class="text-2xl" name='ecs'></div>
<div vslot='menu'>
<div class="dropdown-item" @click='show(0)'>创建</div>
<div class="dropdown-item" @click='show(1)'>高级检索</div>
<div class="dropdown-item" @click='show(2)'>智能导入</div>
</div>
</div>
</vslot>
<div class="table-value" :odd='index%2' v-for='(row, index) in data'>
<vslot class="w-full flex justify-center gap-2 text-xl" name='_addon' v='row,index'>
<div refu='icon' name='edit-square' color='#78c' v-if='!row._enable' @click='row._enable=true'></div>
<div refu='icon' name='save' color='#ff3300' v-else @click='wrap(1, row)'></div>
<div refu='icon' name='delete' color='#f66' v-if='!row._enable' @click='wrap(3, row)'></div>
<div refu='icon' name='close' color='#aaa' v-else @click='delete row._enable'></div>
</vslot>
</div>
</div>
</div>
<div class="flex items-center gap-2 px-4 select-none h-12">
<input !value='listOpts.keyword' @input.delay1s='search' class="keysearch" placeholder="简单检索" />
<div refu='icon' name='left' class="ml-auto" @click='wrap(0,-1)'></div>
<div>{{listOpts.page}}</div>
<div refu='icon' name='right' class="mr-auto" @click='wrap(0,1)'></div>
<div class="">总计{{total}}条数据</div>
</div>
<div refu='dialog' v:show='showFlag'>
<div class="dialog">
<vslot v-if='showMode==0' name='create' v='keys,oncreate'>
<table-create :keys='keys' :oncreate='oncreate'></table-create>
</vslot>
<table-setting :keys='keys' :opts='listOpts' v-else-if='showMode==1' :apply='()=>wrap(0)'>
</table-setting>
<div v-else-if class="w-full flex flex-col flex-grow items-center gap-4" style=" height: calc(100% - 0px);">
<textarea class="w-full bg-gray-200 flex-grow p-4" placeholder="请输入文本内容或者拖入文件" !value='ai_content'
@input='ai_content=$event.target.value' style="resize:vertical;"></textarea>
<div refu='btn' class="mx-auto" size='lg' :click='ai'>智能识别</div>
</div>
</div>
</div>
</body>
<script setup>
searchQuery = '';
records = [
{ id: 1, recordNumber: 'REC-001', name: '张三', status: 'active', createdAt: '2024-01-15', category: '重要客户', type: '企业' },
{ id: 2, recordNumber: 'REC-002', name: '李四', status: 'pending', createdAt: '2024-02-20', category: '普通客户', type: '个人' },
{ id: 3, recordNumber: 'REC-003', name: '王五', status: 'inactive', createdAt: '2024-03-10', category: '潜在客户', type: '企业' },
];
currentRecord = {};
isEditMode = false;
filteredRecords = () => {
return records.filter((record) =>
record.recordNumber.includes(searchQuery) || record.name.includes(searchQuery)
);
};
openModal = (modalId, mode, recordData) => {
const modal = document.getElementById(modalId);
if (!modal) return;
if (mode === 'add') {
isEditMode = false;
currentRecord = { recordNumber: '自动生成', status: 'active', category: '', type: '' };
} else if (mode === 'edit') {
isEditMode = true;
currentRecord = { ...recordData };
} else if (mode === 'view') {
currentRecord = { ...recordData };
showFlag = false
showMode = 0
loading = false
keys = []
data = []
host = window.location.origin
api = ''
editable = false
ai_content = ''
show = (m) => {
showMode = m
showFlag = true
}
modal.classList.remove('hidden');
modal.classList.add('flex');
requestAnimationFrame(() => {
const backdrop = modal.querySelector('.modal-backdrop') || modal;
const content = modal.querySelector('.modal-content');
backdrop.style.opacity = '1';
if (content) content.style.transform = 'scale(1)';
});
document.body.style.overflow = 'hidden';
};
closeModal = (modalId) => {
const modal = document.getElementById(modalId);
if (!modal) return;
const backdrop = modal.querySelector('.modal-backdrop') || modal;
const content = modal.querySelector('.modal-content');
backdrop.style.opacity = '0';
if (content) content.style.transform = 'scale(0.95)';
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
if (!document.querySelector('.modal-backdrop:not(.hidden)')) {
document.body.style.overflow = 'auto';
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}, 300);
};
saveRecord = () => {
if (isEditMode) {
const index = records.findIndex((record) => record.id === currentRecord.id);
records[index] = { ...currentRecord };
update = async (row) => {
if (api) {
return await $env.api.Patch(host + api + '/' + row.id, row)
}
}
total = 0
listOpts = {
page: 1,
page_size: 10,
keyword: '',
keywords: {},
sort_by: 'created_at',
order: 'desc',
}
next = async (opts) => {
if (api) {
opts = Object.assign({}, opts)
if (opts.keywords && Object.keys(opts.keywords).length > 0) {
opts.keywords = JSON.stringify(opts.keywords)
} else {
currentRecord.id = records.length + 1;
records.push({ ...currentRecord });
delete opts.keywords
}
return await $axios.get(host + api, opts)
}
return []
}
create = async (data) => {
if (api) {
return await $axios.post(host + api, data)
}
return
}
del = async (row) => {
if (api) {
return await $axios.delete(host + api + '/' + row.id, row)
}
}
search = (e) => {
listOpts.keyword = e.target.value
wrap(0)
}
oncreate = async (d) => {
await wrap(2, d)
}
wrap = async (mode, props) => {
try {
if (mode === 0) {
if (typeof props === 'number') {
listOpts.page += props
}
let max = Math.ceil(total / listOpts.page_size)
if (listOpts.page < 1) {
listOpts.page = 1
return
} else if (listOpts.page > max && max > 0) {
listOpts.page = max
return
}
let data = await $data.next(listOpts)
if (Array.isArray(data)) {
$data.data = data
} else {
$data.data = data.items
$data.total = data.total
}
} else if (mode === 1) {
await update(props)
props._enable = false
} else if (mode === 2) {
let res = await create(props)
showFlag = false
$data.data.push(res)
} else if (mode == 3) {
await del(props)
$data.data = $data.data.filter((d) => d.id !== data.id)
}
showFlag = false
} catch (e) {
$data.onerr(e)
}
console.log('done')
}
onerr = (e) => {
console.error(e)
}
closeModal('addEditModal');
};
confirmDelete = () => {
records = records.filter((record) => record.id !== currentRecord.id);
closeModal('deleteConfirmModal');
};
statusClass = (status) => {
if (status === 'active') return 'bg-green-100 text-green-800';
if (status === 'pending') return 'bg-yellow-100 text-yellow-800';
if (status === 'inactive') return 'bg-red-100 text-red-800';
};
</script>
<script>
// 初始化:确保所有弹窗开始时都是隐藏的,并设置初始状态
const modals = document.querySelectorAll('.modal-backdrop');
modals.forEach((modal) => {
modal.classList.add('hidden');
modal.style.opacity = '0';
const content = modal.querySelector('.modal-content');
if (content) {
content.style.transform = 'scale(0.95)';
}
});
// 点击弹窗外部区域关闭 (可选)
window.addEventListener('click', (event) => {
const modals = document.querySelectorAll('.modal-backdrop');
modals.forEach((modal) => {
if (event.target === modal) {
closeModal(modal.id);
}
});
});
if ($data.data.length === 0 && $data.api) {
wrap(0)
}
</script>
</html>

@ -0,0 +1,34 @@
import routes from './routes.js'
import token from './token.js'
export default ($env) => {
console.log($env, $vyes.root)
token.setBaseUrl($env.root)
token.wrapAxios($env.$axios)
$env.$global.token = token
$env.$global.user = token.body()
$env.$router.addRoutes(routes)
$env.$router.beforeEnter = async (to, from, next) => {
if (to.meta && to.meta.auth) {
if (token.isExpired()) {
await token.refresh()
}
if (token.isExpired()) {
token.logout(to.path)
}
} else {
next();
}
};
$env.$axios.interceptors.response.use(function(response) {
if (response?.data) {
return response.data
}
return response;
}, function(error) {
let data = error.response ? error.response.data : error.response
return Promise.reject(data.message || data);
});
}

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<title>Auth Layout</title>
<style>
.layout-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
.header {
user-select: none;
height: 60px;
background: #409EFF;
color: white;
display: flex;
align-items: center;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header-title {
font-size: 18px;
font-weight: bold;
}
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
.menu {
width: 200px;
border-right: 1px solid #ddd;
padding: 20px 0;
}
.menu a {
padding: 10px 20px;
cursor: pointer;
transition: background 0.3s;
display: block;
color: inherit;
text-decoration: none;
}
.menu a:hover {
background: #e0e0e0;
}
.menu a[active] {
background: #409EFF;
}
.content {
flex: 1;
overflow-y: auto;
width: 100%;
height: 100%;
}
.footer {
height: 40px;
background: #f5f5f5;
border-top: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #666;
}
</style>
</head>
<body style="height: 100%;width: 100%;margin: 0;">
<div class="layout-container">
<header class="header">
<div class="header-title">
<a href="@/">首页</a>
</div>
<a class="ml-auto" href="/">应用权限管理</a>
<!-- 新增跳转到/home的链接 -->
<div refu='ico' class="ml-auto" style="margin-left: auto;"></div>
</header>
<div class="main-container">
<vslot v='user' class="menu" name='menu'>
<a :href="`/app/${id}`">应用简介</a>
<a :href="`/app/${id}/user`">用户管理</a>
<a :href="`/app/${id}/auth`">权限管理</a>
<a :href="`/app/${id}/settings`">应用设置</a>
</vslot>
<vslot class="content">
</vslot>
</div>
<footer class="footer">
Copyright © 2025 veypi. All Rights Reserved..
</footer>
</div>
</body>
<script setup>
id = $router.params.id;
try {
$env.app = await $axios.get(`/api/app/${id}`)
document.title = `${app.name} - 项目主页`
} catch (e) {
$router.push('/')
console.log(e)
}
</script>
</html>

@ -35,25 +35,26 @@
.menu {
width: 200px;
background: #f5f5f5;
border-right: 1px solid #ddd;
padding: 20px 0;
}
.menu-item {
.menu a {
padding: 10px 20px;
cursor: pointer;
transition: background 0.3s;
display: block;
color: inherit;
text-decoration: none;
}
.menu-item:hover {
.menu a:hover {
background: #e0e0e0;
}
.menu-item a {
display: block;
color: inherit;
text-decoration: none;
.menu a[active] {
background: #409EFF;
}
.content {
@ -90,15 +91,9 @@
<div class="main-container">
<vslot v='user' class="menu" name='menu'>
<div class="menu-item">
<a href="/app">应用管理</a>
</div>
<div class="menu-item">
<a href="/profile">个人中心</a>
</div>
<div class="menu-item">
<a href="/app" class="">应用管理</a>
<a href="/profile" class="">个人中心</a>
<a href="/settings">系统设置</a>
</div>
</vslot>
<vslot class="content">

@ -256,7 +256,6 @@
<body>
<div class="container">
<header>
<h1>权限应用管理</h1>
<div class="search-bar">
@ -309,7 +308,7 @@
<div class="app-card-footer">
<div class="app-actions">
<a :href="app.init_url || '#'" class="btn btn-primary">打开应用</a>
<a :href="`/app/main?id=${app.id}`">
<a :href="`/app/${app.id}`">
<button class="btn btn-outline">详情</button>
</a>
</div>
@ -317,7 +316,6 @@
</div>
</div>
</main>
</div>
</body>
<script setup>

@ -36,46 +36,46 @@
</style>
<body>
<c-app-menu slot="menu"></c-app-menu>
<div class="header">应用权限管理</div>
<div class="sub-header">资源表</div>
<v-table :onerr="onerr" :keys="keys" :api='resource_url'></v-table>
<v-table :axios='$axios' :onerr="onerr" :keys="keys" :api='resource_url'></v-table>
<div class="sub-header">角色表</div>
<v-table :onerr="onerr" :keys="role_keys" :api='role_url'>
<v-btn slot='_addon' size='sm' @click='show_user(row)' color='#2c9'>权限表</v-btn>
<v-table :axios='$axios' :onerr="onerr" :keys="role_keys" :api='role_url'>
<v-btn vslot='_addon' size='sm' @click='show_user(row)' color='#2c9'>权限表</v-btn>
</v-table>
<v-dialog v:show='show'>
<div class="dialog">
<div class="text-2xl">{{selected_role.name}}角色权限表</div>
<v-table :onerr="onerr" :keys="access_keys" :api='access_url' :data="accessData"
:next='access_api.next'></v-table>
<v-table :querys='{role_id: selected_role?.id}' :axios='$axios' :onerr="onerr" :keys="access_keys"
:api='access_url' :data="accessData"></v-table>
</div>
</v-dialog>
</body>
<script setup>
id = $router.params.id
rows = []
keys = [
{name: 'id', label: 'ID', no_create: true},
{name: 'id', label: 'ID', disabled: true, style: {width: '2rem'}},
{name: 'name', required: true, label: '资源名称'},
{name: 'des', label: '资源描述', editable: true},
{name: 'created_at', label: '创建时间', no_create: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', no_create: true, field: (r) => new Date(r.updated_at).toLocaleString()},
{name: 'created_at', label: '创建时间', disabled: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', disabled: true, field: (r) => new Date(r.updated_at).toLocaleString()},
]
role_keys = [
{name: 'id', label: 'ID', no_create: true},
{name: 'id', label: 'ID', disabled: true, style: {width: '2rem'}},
{name: 'name', required: true, label: '角色名称'},
{name: 'des', required: true, label: '角色描述', editable: true},
{name: 'user_count', label: '用户数量', no_create: true},
{name: 'created_at', label: '创建时间', no_create: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', no_create: true, field: (r) => new Date(r.updated_at).toLocaleString()},
{name: 'user_count', label: '用户数量', disabled: true},
{name: 'created_at', label: '创建时间', disabled: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', disabled: true, field: (r) => new Date(r.updated_at).toLocaleString()},
]
access_keys = [
{name: 'id', label: 'ID', no_create: true},
{name: 'id', label: 'ID', disabled: true, style: {width: '2rem'}},
{name: 'name', required: true, label: '资源名'},
{name: 'tid', label: '限制域', editable: true},
{name: 'level', label: '权限等级', editable: true},
{name: 'created_at', label: '创建时间', no_create: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', no_create: true, field: (r) => new Date(r.updated_at).toLocaleString()},
{name: 'created_at', label: '创建时间', disabled: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', disabled: true, field: (r) => new Date(r.updated_at).toLocaleString()},
]
show = false
role_user_keys = []
@ -84,31 +84,17 @@
user_role_data = []
show_user = async (row) => {
selected_role = row
user_role_url = ($env.root || '') + `/api/user/_/user_role/${selected_role.id}`
accessData = await access_api.next(0, 10)
// user_role_data = await user_role_api.next(0, 10)
access_url = `/api/app/${id}/access`
show = true
}
const params = new URLSearchParams(window.location.search)
id = params.get('id')
if (!id) {
history.back()
return
}
resource_url = ($env.root || '') + `/api/app/${id}/resource`
role_url = ($env.root || '') + `/api/app/${id}/role`
access_url = ($env.root || '') + `/api/access`
user_role_url = ($env.root || '') + `/api/user/_/user_role/${selected_role.id}`
resource_url = `/api/app/${id}/resource`
access_url = ``
role_url = `/api/app/${id}/role`
user_role_url = `/api/user/_/user_role/${selected_role.id}`
onerr = (e) => {
console.warn(e)
history.back()
}
access_api = {
next: async (page, size) => {
console.log(id)
return await $api.Get(access_url, {query: {page, size, app_id: id, role_id: selected_role.id || ''}})
}
// history.back()
}
</script>

@ -161,7 +161,6 @@
</style>
<body>
<c-app-menu slot="menu"></c-app-menu>
<div class="container">
<header>
<div class="header-content">
@ -223,11 +222,11 @@
<div class="date-stat">
<div class="date-item">
<div class="date-label">创建于</div>
<div class="date-value">{{formatDate(app.created_at) || 'N/A'}}</div>
<!-- <div class="date-value">{{formatDate(app.created_at) || 'N/A'}}</div> -->
</div>
<div class="date-item">
<div class="date-label">最后更新</div>
<div class="date-value">{{formatDate(app.updated_at) || 'N/A'}}</div>
<!-- <div class="date-value">{{formatDate(app.updated_at) || 'N/A'}}</div> -->
</div>
</div>
</div>
@ -252,7 +251,7 @@
}
id = $router.params.id
if (!id) {
$router.back()
$router.push('/app')
return
}
@ -281,7 +280,7 @@
})
.catch((e) => {
console.log(e)
history.back()
$router.push('/app')
})
}
sync()

@ -28,29 +28,26 @@
</style>
<body>
<c-app-menu slot="menu"></c-app-menu>
<div class="header">应用用户管理</div>
<v-table :data="rows" :keys="keys" :api='user_api'>
<v-btn slot='_addon' size='sm' @click='show_user(row)' color='#2c9'>权限表</v-btn>
<v-table :axios='$axios' :keys="keys" api='/api/user'>
<v-btn vslot='_addon' size='sm' @click='show_user(row)' color='#2c9'>权限表</v-btn>
</v-table>
<v-dialog v:show='show'>
<div class="dialog">
<div class="text-2xl">{{selected.username}} 角色表</div>
<v-table :onerr="onerr" :keys="au_keys" :data="user_role_data"></v-table>
<v-table :axios='$axios' :keys="au_keys" :api='`/api/user/${app.id}/user_role`'></v-table>
</div>
</v-dialog>
</body>
<script setup>
show = false
const params = new URLSearchParams(window.location.search)
id = params.get('id')
console.log($env)
auOpts = {
0: ['正常', 'positive'],
1: ['拒绝', 'warning'],
2: ['申请中', 'primary'],
3: ['禁用', 'warning'],
}
user_api = ($env.root || '') + '/api/user'
keys = [
{
name: 'id',
@ -76,68 +73,28 @@
]
au_keys = [
{name: 'id', label: 'ID', no_create: true},
{name: 'username', required: true, label: '用户名'},
{name: 'role_name', label: 'role_name'},
{name: 'created_at', label: '创建时间', no_create: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', no_create: true, field: (r) => new Date(r.updated_at).toLocaleString()},
{name: 'status', label: '账号状态', sortable: true},
]
rows = []
user_role_data = []
selected = {}
show_user = async (row) => {
selected = row
console.log(row)
user_role_url = ($env.root || '') + `/api/user/${Guser.uid}/user_role`
user_role_url = `/api/user/${row.id}/user_role`
// accessData = await access_api.next(0, 10)
user_role_data = await user_role_api.next(0, 10)
// user_role_data = await user_role_api.next(0, 10)
show = true
}
user_role_api = {
next: async (page, size) => {
return await $api.Get(user_role_url, {query: {page, size}})
return await $axios.get(user_role_url, {params: {page, size}})
}
}
reset = (id) => {
$q.dialog({
title: '是否重置密码',
message: '请谨慎操作',
cancel: true,
persistent: true
}).onOk(() => {
$api.user.reset(id).then(() => {
msg.Info('重置成功 ')
}).catch((e) => {
msg.Warn('失败 ' + e)
})
})
}
app = {}
sync = () => {
const params = new URLSearchParams(window.location.search)
id = params.get('id')
if (!id) {
history.back()
return
}
$api.Get(`/api/app/${id}`)
.then((data) => {
Object.assign(app, data)
document.title = `${app.name} - 项目主页`
$api.Get(`/api/user/`).then((e) => {
rows = e
console.log(e)
})
})
.catch((e) => {
console.log(e)
history.back()
})
}
sync()
</script>
</html>

@ -327,7 +327,8 @@
<script setup>
// 响应式数据
redirect = new URLSearchParams(window.location.search).get('redirect') || '/'
logout = $router.query.logout
redirect = $router.query.redirect || '/'
isSignUp = false;
signUpForm = {username: '', password: ''};
signInForm = {username: '', password: ''};

@ -8,16 +8,6 @@
</head>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
height: 100%;
width: 100%;
}
.profile-container {
background-color: white;
border-radius: 8px;
padding: 20px;
display: flex;
@ -52,6 +42,7 @@
height: 100px;
border-radius: 50%;
object-fit: cover;
margin: 0 auto;
margin-bottom: 10px;
}
@ -77,7 +68,6 @@
</style>
<body>
<div class="profile-container">
<h1 class="text-2xl text-center">个人信息修改</h1>
<div class="form-group">
@ -110,7 +100,7 @@
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
<div v-if="successMessage" style="color: green; margin-top: 10px;">{{ successMessage }}</div>
</div>
</body>
<script setup>
// 初始化用户数据
@ -183,7 +173,7 @@
};
}
} catch (error) {
errorMessage = "保存失败: " + error.message;
errorMessage = "保存失败: " + error;
} finally {
isLoading = false;
}
@ -193,6 +183,5 @@
<script>
loadUserData();
</script>
</body>
</html>

@ -5,53 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>oa</title>
<script type="module" key='vyes' src="http://test2.vyesai.com/vyes/v.js"></script>
<link rel="stylesheet" href="/assets/common.css">
<link href="/assets/libs/tailwind/tailwind.min.css" rel="stylesheet">
<link href="/assets/libs/animate/animate.min.css" rel="stylesheet">
<link href="/assets/libs/font-awesome/css/all.min.css" rel="stylesheet">
</head>
<body root class="h-full w-full">
<vrouter class="h-full w-full block">
<body root>
<vrouter>
<page-404></page-404>
</vrouter>
</body>
<script type='module' setup>
if (typeof $env !== 'undefined') {
const token = (await import(root + '/token.js')).default
token.setRoot(root)
token.wrapAxios($axios)
let user = token.body()
$env.Guser = user
$env.Gtoken = token.getToken()
$router.beforeEnter = async (to, from, next) => {
if (to.meta && to.meta.auth) {
// check if the user is authenticated
// next({ path: '/login' });
if (token.isExpired()) {
await token.refresh()
}
if (token.isExpired()) {
token.logout()
}
} else {
next()
}
};
$axios.interceptors.response.use(function (response) {
if (response.data && response.data.code === 0) {
return response.data.data
}
return response;
}, function (error) {
let data = error.response ? error.response.data : error.response
if (!data) return error.response
if (data.code >= 400) {
return Promise.reject(data.message)
}
return Promise.reject(data.message || data);
});
}
</script>
</html>

@ -10,9 +10,14 @@ const routes = [
{ path: '/login', component: '/page/login.html', name: 'login', meta: { auth: false } },
{ path: '/profile', component: '/page/profile.html', name: 'profile', meta: { auth: true } },
{ path: '/app', component: '/page/app.html', name: 'app', meta: { auth: true } },
{ path: '/settings', component: '/page/settings.html', name: 'settings', meta: { auth: true } },
{
path: '/app/:app_id', children: [
{ path: '/', component: '/page/app/index.html', name: 'app_index', meta: { auth: true } },
path: '/app/:id', layout: 'app', meta: { auth: true },
children: [
{ path: '/', component: '/page/app/index.html' },
{ path: '/user', component: '/page/app/user.html' },
{ path: '/auth', component: '/page/app/auth.html' },
{ path: '/settings', component: '/page/app/settings.html' },
]
},
{ path: '*', component: '/page/404.html', name: '404' },

@ -6,13 +6,13 @@
*/
class TokenService {
__root = ''
#url = '/'
constructor() {
this.tokenKey = 'access';
this.refreshTokenKey = 'refresh';
}
setRoot(root) {
this.__root = root;
setBaseUrl(url) {
this.#url = url;
}
setToken(token) {
@ -71,7 +71,9 @@ class TokenService {
}
logout(to) {
this.clearToken();
location.href = this.__root + '/login?redirect=' + (to || window.location.pathname);
let url = this.#url + '/login';
url += '?redirect='
location.href = url + (to || window.location.pathname);
}
async refresh() {
@ -81,17 +83,13 @@ class TokenService {
return;
}
try {
let data = await fetch(this.__root + '/api/token', {
let data = await fetch(this.#url + '/api/token', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh: refreshToken })
}).then(res => res.json())
this.__cache = null; // 清除缓存
if (data.code === 0) {
this.setToken(data.data);
} else {
this.clearToken()
}
this.setToken(data);
} catch (e) {
console.error('Token刷新失败:', e);
this.clearToken()

Loading…
Cancel
Save