feat: change auth check

v3
veypi 3 months ago
parent 96acf05fb6
commit ada216cfd5

@ -9,13 +9,14 @@ package app
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/models"
"github.com/vyes/vigo"
"github.com/vyes/vigo/contrib/crud"
)
var Router = vigo.NewRouter()
var appRouter = Router.SubRouter(":app_id")
var appRouter = Router.SubRouter(":app_id").UseBefore(auth.Check("app", ":app_id", 2))
func init() {
crud.All(appRouter.SubRouter("resource"), cfg.DB, models.Resource{})

@ -11,11 +11,12 @@ import (
"github.com/veypi/OneAuth/api/app"
"github.com/veypi/OneAuth/api/token"
"github.com/veypi/OneAuth/api/user"
"github.com/veypi/OneAuth/libs/auth"
"github.com/vyes/vigo"
"github.com/vyes/vigo/contrib/common"
)
var Router = vigo.NewRouter().UseAfter(common.JsonResponse, common.JsonErrorResponse)
var Router = vigo.NewRouter().UseBefore(auth.CheckJWT).UseAfter(common.JsonResponse, common.JsonErrorResponse)
var (
_ = Router.Extend("user", user.Router)

@ -8,7 +8,7 @@ import (
"github.com/vyes/vigo"
)
var _ = Router.Patch("/:token_id", tokenPatch)
// var _ = Router.Patch("/:token_id", tokenPatch)
type patchOpts struct {
ID string `json:"id" parse:"path@token_id"`

@ -30,7 +30,7 @@ type postOpts struct {
Device *string `json:"device" parse:"json"`
}
var _ = Router.Post("/", tokenPost)
var _ = Router.Post("/", vigo.SkipBefore, tokenPost)
// for user login app
func tokenPost(x *vigo.X) (any, error) {

@ -17,7 +17,7 @@ type getOpts struct {
ID string `json:"id" parse:"path@token_id"`
}
var _ = Router.Get("/:token_id", tokenGet)
// var _ = Router.Get("/:token_id", tokenGet)
func tokenGet(x *vigo.X) (any, error) {
opts := &getOpts{}
@ -36,7 +36,7 @@ type listOpts struct {
AppID string `json:"app_id" gorm:"index;type:varchar(32)" parse:"query"`
}
var _ = Router.Get("/", tokenList)
// var _ = Router.Get("/", tokenList)
func tokenList(x *vigo.X) (any, error) {
opts := &listOpts{}

@ -23,7 +23,7 @@ import (
"gorm.io/gorm"
)
var _ = Router.Post("/", userPost)
var _ = Router.Post("/", vigo.SkipBefore, publicLimits, userPost)
type postOpts struct {
Username string `json:"username" gorm:"varchar(100);unique;default:not null" parse:"json"`

@ -9,14 +9,15 @@ package user
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/models"
"github.com/vyes/vigo"
"github.com/vyes/vigo/contrib/crud"
)
var Router = vigo.NewRouter()
var userRouter = Router.SubRouter("/:user_id").UseBefore(userGet)
func init() {
crud.All(Router.SubRouter("/:user_id/user_role"), cfg.DB, models.UserRole{})
crud.All(Router.SubRouter("/:user_id/user_role"), cfg.DB, models.UserRole{}).UseBefore(auth.Check("user", "", 4))
}

@ -17,10 +17,19 @@ import (
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"github.com/vyes/vigo"
"github.com/vyes/vigo/contrib/limiter"
"github.com/vyes/vigo/logv"
)
var _ = Router.Post("/login", userLogin)
var publicLimits = limiter.NewAdvancedRequestLimiter(limiter.LimiterConfig{
Window: time.Minute * 5,
MaxRequests: 20,
MinInterval: time.Second * 3,
}).Limit
var _ = Router.Post("/login",
vigo.SkipBefore, publicLimits,
userLogin)
type loginOpts struct {
UserName string `json:"username" parse:"json"`

@ -8,7 +8,7 @@ import (
"github.com/vyes/vigo/contrib/crud"
)
var _ = Router.Delete("/:user_id", auth.Check("user", "user_id", auth.DoDelete), userDelete)
var _ = Router.Delete("/:user_id", auth.Check("user", "user_id", auth.DoDelete, checkOwner), userDelete)
func userDelete(x *vigo.X) (any, error) {
data := &models.User{}
@ -16,65 +16,22 @@ func userDelete(x *vigo.X) (any, error) {
return data, err
}
var _ = Router.Get("/:user_id", auth.Check("user", "user_id", auth.DoRead), userGet)
var _ = Router.Get("/:user_id", auth.Check("user", "user_id", auth.DoRead, checkOwner), vigo.DiliverData)
type getOpts struct {
ID string `json:"id" parse:"path@user_id"`
func checkOwner(x *vigo.X, data any) bool {
u, ok1 := data.(*models.User)
return ok1 && u.ID == x.Params.Get("user_id")
}
func userGet(x *vigo.X) (any, error) {
opts := &getOpts{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &models.User{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
err := cfg.DB().Where("id = ?", x.Params.Get("user_id")).First(data).Error
return data, err
}
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"`
Nickname *string `json:"nickname" parse:"query"`
Email *string `json:"email" parse:"query"`
Phone *string `json:"phone" parse:"query"`
Status *uint `json:"status" parse:"query"`
}
var _ = Router.Get("/", "list user", auth.Check("user", "", auth.DoUpdate), crud.List(cfg.DB, &models.User{}))
func userList(x *vigo.X) (any, error) {
opts := &listOpts{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*models.User, 0, 10)
query := cfg.DB()
if opts.Username != nil {
query = query.Where("username LIKE ?", opts.Username)
}
if opts.Nickname != nil {
query = query.Where("nickname LIKE ?", opts.Nickname)
}
if opts.Email != nil {
query = query.Where("email LIKE ?", opts.Email)
}
if opts.Phone != nil {
query = query.Where("phone LIKE ?", opts.Phone)
}
if opts.Status != nil {
query = query.Where("status = ?", opts.Status)
}
err = query.Find(&data).Error
return data, err
}
var _ = Router.Patch("/:user_id", auth.Check("user", "user_id", auth.DoUpdate), userPatch)
var _ = Router.Patch("/:user_id", auth.Check("user", "user_id", auth.DoUpdate, checkOwner), userPatch)
type patchOpts struct {
ID string `json:"id" parse:"path@user_id"`
@ -87,18 +44,14 @@ type patchOpts struct {
Status *uint `json:"status" parse:"json"`
}
func userPatch(x *vigo.X) (any, error) {
func userPatch(x *vigo.X, args any) (any, error) {
opts := &patchOpts{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &models.User{}
data := args.(*models.User)
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]any)
if opts.Username != nil {
optsMap["username"] = opts.Username

@ -21,4 +21,6 @@ var Config = &Options{
TokenExpire: time.Second * 10,
ID: "test",
Key: "asdfghjklqwertyuiopzxcvbnm1234567890",
DSN: "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local",
DB: "mysql",
}

@ -86,20 +86,33 @@ func CheckJWT(x *vigo.X) (any, error) {
return checkJWT(x)
}
func Check(target string, pid string, l AuthLevel) func(x *vigo.X) (any, error) {
return func(x *vigo.X) (any, error) {
claims, err := checkJWT(x)
if err != nil {
return nil, err
// return nil, err
type CustomCheckFunc = func(x *vigo.X, data any) bool
func Check(target string, pid string, l AuthLevel, funcs ...CustomCheckFunc) func(x *vigo.X, data any) (any, error) {
return func(x *vigo.X, data any) (any, error) {
var err error
claims, ok := x.Get("token").(*Claims)
if !ok {
claims, err = checkJWT(x)
if err != nil {
return nil, err
}
}
tid := ""
if pid != "" {
tid = x.Params.Get(pid)
if strings.HasPrefix(pid, "@") {
tid, _ = x.Get(pid[1:]).(string)
}
if strings.HasPrefix(pid, ":") {
tid = x.Params.Get(pid[1:])
}
if !claims.Access.Check(target, tid, l) {
return nil, AuthNoPerm
err = AuthNoPerm
}
for _, fn := range funcs {
if fn(x, data) {
return data, nil
}
}
return claims, nil
return data, err
}
}

@ -6,7 +6,8 @@ export default ($env) => {
token.setBaseUrl($env.root)
token.wrapAxios($env.$axios)
$env.$G.token = token
$env.$G.user = token.body()
let user = token.body()
$env.$G.user = user
$env.$router.addRoutes(routes)
@ -18,17 +19,18 @@ export default ($env) => {
if (token.isExpired()) {
token.logout(to.fullPath)
}
if (!token.check('app', '', 2)) {
next('/')
}
} else {
next();
}
};
$env.$axios.interceptors.response.use(function(response) {
if (response?.data) {
return response.data
}
return response;
return response?.data || response;
}, function(error) {
let data = error.response ? error.response.data : error.response
return Promise.reject(data.message || data);
console.error('Axios Error:', error);
error = error?.response?.data || error?.response || error
return Promise.reject(error.message || error);
});
}

@ -1,696 +1,9 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<title>项目与用户管理 Dashboard</title>
<script src="/assets/libs/echarts.min.js"></script>
</head>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f7fa;
color: #333;
}
.dashboard {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 28px;
color: #2c3e50;
margin: 0;
}
.header .date {
color: #7f8c8d;
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: transform 0.3s, box-shadow 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.stat-card .icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
font-size: 20px;
}
.stat-card .value {
font-size: 28px;
font-weight: 700;
margin-bottom: 5px;
}
.stat-card .label {
color: #7f8c8d;
font-size: 14px;
}
.stat-card .trend {
display: flex;
align-items: center;
margin-top: 10px;
font-size: 13px;
}
.trend.up {
color: #2ecc71;
}
.trend.down {
color: #e74c3c;
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.chart-container h2 {
margin-top: 0;
font-size: 18px;
color: #2c3e50;
}
.chart {
width: 100%;
height: 300px;
}
.recent-activities {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.recent-activities h2 {
margin-top: 0;
font-size: 18px;
color: #2c3e50;
}
.activity-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid #eee;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
color: #3b82f6;
}
.activity-content {
flex: 1;
}
.activity-title {
font-weight: 600;
margin-bottom: 5px;
}
.activity-time {
color: #7f8c8d;
font-size: 13px;
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.badge-primary {
background: #e3f2fd;
color: #1976d2;
}
.badge-success {
background: #e8f5e9;
color: #388e3c;
}
.badge-warning {
background: #fff8e1;
color: #ffa000;
}
</style>
<body>
<div class="dashboard">
<div class="header">
<h1>项目与用户管理 Dashboard</h1>
<div class="date">{{ currentDate }}</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="icon" style="background: #e3f2fd; color: #1976d2;">
<i class="bi bi-people"></i>
</div>
<div class="value">{{ totalUsers }}</div>
<div class="label">总用户数</div>
<div class="trend up">
<i class="bi bi-arrow-up"></i> {{ userGrowthRate }}% 较上月
</div>
</div>
<div class="stat-card">
<div class="icon" style="background: #e8f5e9; color: #388e3c;">
<i class="bi bi-folder"></i>
</div>
<div class="value">{{ activeProjects }}</div>
<div class="label">活跃项目</div>
<div class="trend up">
<i class="bi bi-arrow-up"></i> {{ projectGrowthRate }}% 较上月
</div>
</div>
<div class="stat-card">
<div class="icon" style="background: #fff8e1; color: #ffa000;">
<i class="bi bi-clock-history"></i>
</div>
<div class="value">{{ pendingTasks }}</div>
<div class="label">待处理任务</div>
<div class="trend down">
<i class="bi bi-arrow-down"></i> {{ taskChangeRate }}% 较上周
</div>
</div>
<div class="stat-card">
<div class="icon" style="background: #f3e5f5; color: #8e24aa;">
<i class="bi bi-graph-up"></i>
</div>
<div class="value">{{ completionRate }}%</div>
<div class="label">项目完成率</div>
<div class="trend up">
<i class="bi bi-arrow-up"></i> {{ completionRateChange }}% 较上月
</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<h2>用户增长趋势</h2>
<div id="userGrowthChart" class="chart"></div>
</div>
<div class="chart-container">
<h2>项目状态分布</h2>
<div id="projectStatusChart" class="chart"></div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<h2>用户活跃度</h2>
<div id="userActivityChart" class="chart"></div>
</div>
<div class="chart-container">
<h2>任务完成情况</h2>
<div id="taskCompletionChart" class="chart"></div>
</div>
</div>
<div class="recent-activities">
<h2>最近活动</h2>
<div v-for="activity in recentActivities" class="activity-item">
<div class="activity-icon">
<i :class="'bi bi-' + activity.icon"></i>
</div>
<div class="activity-content">
<div class="activity-title">
{{ activity.title }}
<span v-if="activity.type" :class="'badge badge-' + activity.type">{{ activity.typeLabel }}</span>
</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
</div>
</div>
<script setup>
// 统计数据
totalUsers = 1248;
activeProjects = 86;
pendingTasks = 342;
completionRate = 78;
// 增长率
userGrowthRate = 12.5;
projectGrowthRate = 8.3;
taskChangeRate = 5.2;
completionRateChange = 3.7;
// 当前日期
currentDate = new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
// 最近活动数据
recentActivities = [
{
icon: 'plus-circle',
title: '新用户 "张小明" 注册',
type: 'primary',
typeLabel: '用户',
time: '10分钟前'
},
{
icon: 'folder-plus',
title: '新项目 "电商平台重构" 创建',
type: 'success',
typeLabel: '项目',
time: '35分钟前'
},
{
icon: 'check-circle',
title: '项目 "CRM系统升级" 完成',
type: 'success',
typeLabel: '项目',
time: '2小时前'
},
{
icon: 'exclamation-triangle',
title: '任务 "用户权限模块" 逾期',
type: 'warning',
typeLabel: '任务',
time: '5小时前'
},
{
icon: 'person-plus',
title: '团队成员 "李华" 加入项目',
type: 'primary',
typeLabel: '团队',
time: '昨天'
}
];
// 图表数据
userGrowthData = {
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月'],
values: [820, 890, 920, 950, 1020, 1120, 1248]
};
projectStatusData = [
{value: 32, name: '进行中'},
{value: 24, name: '已完成'},
{value: 18, name: '规划中'},
{value: 12, name: '已暂停'}
];
userActivityData = {
days: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
activeUsers: [420, 532, 601, 534, 630, 410, 320],
newUsers: [120, 132, 101, 134, 90, 60, 40]
};
taskCompletionData = {
weeks: ['第1周', '第2周', '第3周', '第4周'],
completed: [120, 132, 101, 134],
pending: [80, 60, 90, 70]
};
</script>
<script>
// 初始化图表
function initCharts() {
// 用户增长趋势图
const userGrowthChart = echarts.init(document.getElementById('userGrowthChart'));
userGrowthChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: $data.userGrowthData.months,
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
},
splitLine: {
lineStyle: {
color: '#e5e7eb'
}
}
},
series: [{
data: $data.userGrowthData.values,
type: 'line',
smooth: true,
lineStyle: {
width: 4,
color: '#3b82f6'
},
itemStyle: {
color: '#3b82f6',
borderWidth: 2
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(59, 130, 246, 0.5)'
},
{
offset: 1,
color: 'rgba(59, 130, 246, 0.1)'
}
])
}
}]
});
// 项目状态分布图
const projectStatusChart = echarts.init(document.getElementById('projectStatusChart'));
projectStatusChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: {
color: '#6b7280'
}
},
series: [
{
name: '项目状态',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: $data.projectStatusData.map((item, index) => ({
value: item.value,
name: item.name,
itemStyle: {
color: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6'][index]
}
}))
}
]
});
// 用户活跃度图
const userActivityChart = echarts.init(document.getElementById('userActivityChart'));
userActivityChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: ['活跃用户', '新增用户'],
textStyle: {
color: '#6b7280'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: $data.userActivityData.days,
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
}
}
],
yAxis: [
{
type: 'value',
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
},
splitLine: {
lineStyle: {
color: '#e5e7eb'
}
}
}
],
series: [
{
name: '活跃用户',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(59, 130, 246, 0.8)'
},
{
offset: 1,
color: 'rgba(59, 130, 246, 0.1)'
}
])
},
emphasis: {
focus: 'series'
},
data: $data.userActivityData.activeUsers
},
{
name: '新增用户',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(16, 185, 129, 0.8)'
},
{
offset: 1,
color: 'rgba(16, 185, 129, 0.1)'
}
])
},
emphasis: {
focus: 'series'
},
data: $data.userActivityData.newUsers
}
]
});
// 任务完成情况图
const taskCompletionChart = echarts.init(document.getElementById('taskCompletionChart'));
taskCompletionChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['已完成', '待处理'],
textStyle: {
color: '#6b7280'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: $data.taskCompletionData.weeks,
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
},
splitLine: {
lineStyle: {
color: '#e5e7eb'
}
}
},
series: [
{
name: '已完成',
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
},
data: $data.taskCompletionData.completed,
itemStyle: {
color: '#10b981'
}
},
{
name: '待处理',
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
},
data: $data.taskCompletionData.pending,
itemStyle: {
color: '#f59e0b'
}
}
]
});
// 窗口大小变化时重新调整图表大小
window.addEventListener('resize', function () {
userGrowthChart.resize();
projectStatusChart.resize();
userActivityChart.resize();
taskCompletionChart.resize();
});
}
hello
</body>
// 页面加载完成后初始化图表
initCharts()
</script>
</html>

@ -508,23 +508,26 @@
// 加载用户数据
loadUserData = async () => {
try {
isLoading = true
const response = await $axios.get("/api/user/" + user.id)
if (response) {
user = {
id: response.id,
username: response.username || "",
nickname: response.nickname || "",
icon: response.icon || "",
email: response.email || "",
phone: response.phone || "",
status: response.status || 0
}
// 保存原始数据
originalUser = JSON.parse(JSON.stringify(user))
hasChanges = false
isLoading = true
const response = await $axios.get("/api/user/" + user.id).catch(error => {
console.log(error)
})
console.log(response)
if (response) {
user = {
id: response.id,
username: response.username || "",
nickname: response.nickname || "",
icon: response.icon || "",
email: response.email || "",
phone: response.phone || "",
status: response.status || 0
}
// 保存原始数据
originalUser = JSON.parse(JSON.stringify(user))
hasChanges = false
}
try {
} catch (error) {
showError("加载用户数据失败: " + error.message)
} finally {

@ -0,0 +1,696 @@
<!doctype html>
<html>
<head>
<title>项目与用户管理 Dashboard</title>
<script src="/assets/libs/echarts.min.js"></script>
</head>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f7fa;
color: #333;
}
.dashboard {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 28px;
color: #2c3e50;
margin: 0;
}
.header .date {
color: #7f8c8d;
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: transform 0.3s, box-shadow 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.stat-card .icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
font-size: 20px;
}
.stat-card .value {
font-size: 28px;
font-weight: 700;
margin-bottom: 5px;
}
.stat-card .label {
color: #7f8c8d;
font-size: 14px;
}
.stat-card .trend {
display: flex;
align-items: center;
margin-top: 10px;
font-size: 13px;
}
.trend.up {
color: #2ecc71;
}
.trend.down {
color: #e74c3c;
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.chart-container h2 {
margin-top: 0;
font-size: 18px;
color: #2c3e50;
}
.chart {
width: 100%;
height: 300px;
}
.recent-activities {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.recent-activities h2 {
margin-top: 0;
font-size: 18px;
color: #2c3e50;
}
.activity-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid #eee;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
color: #3b82f6;
}
.activity-content {
flex: 1;
}
.activity-title {
font-weight: 600;
margin-bottom: 5px;
}
.activity-time {
color: #7f8c8d;
font-size: 13px;
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.badge-primary {
background: #e3f2fd;
color: #1976d2;
}
.badge-success {
background: #e8f5e9;
color: #388e3c;
}
.badge-warning {
background: #fff8e1;
color: #ffa000;
}
</style>
<body>
<div class="dashboard">
<div class="header">
<h1>项目与用户管理 Dashboard</h1>
<div class="date">{{ currentDate }}</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="icon" style="background: #e3f2fd; color: #1976d2;">
<i class="bi bi-people"></i>
</div>
<div class="value">{{ totalUsers }}</div>
<div class="label">总用户数</div>
<div class="trend up">
<i class="bi bi-arrow-up"></i> {{ userGrowthRate }}% 较上月
</div>
</div>
<div class="stat-card">
<div class="icon" style="background: #e8f5e9; color: #388e3c;">
<i class="bi bi-folder"></i>
</div>
<div class="value">{{ activeProjects }}</div>
<div class="label">活跃项目</div>
<div class="trend up">
<i class="bi bi-arrow-up"></i> {{ projectGrowthRate }}% 较上月
</div>
</div>
<div class="stat-card">
<div class="icon" style="background: #fff8e1; color: #ffa000;">
<i class="bi bi-clock-history"></i>
</div>
<div class="value">{{ pendingTasks }}</div>
<div class="label">待处理任务</div>
<div class="trend down">
<i class="bi bi-arrow-down"></i> {{ taskChangeRate }}% 较上周
</div>
</div>
<div class="stat-card">
<div class="icon" style="background: #f3e5f5; color: #8e24aa;">
<i class="bi bi-graph-up"></i>
</div>
<div class="value">{{ completionRate }}%</div>
<div class="label">项目完成率</div>
<div class="trend up">
<i class="bi bi-arrow-up"></i> {{ completionRateChange }}% 较上月
</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<h2>用户增长趋势</h2>
<div id="userGrowthChart" class="chart"></div>
</div>
<div class="chart-container">
<h2>项目状态分布</h2>
<div id="projectStatusChart" class="chart"></div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<h2>用户活跃度</h2>
<div id="userActivityChart" class="chart"></div>
</div>
<div class="chart-container">
<h2>任务完成情况</h2>
<div id="taskCompletionChart" class="chart"></div>
</div>
</div>
<div class="recent-activities">
<h2>最近活动</h2>
<div v-for="activity in recentActivities" class="activity-item">
<div class="activity-icon">
<i :class="'bi bi-' + activity.icon"></i>
</div>
<div class="activity-content">
<div class="activity-title">
{{ activity.title }}
<span v-if="activity.type" :class="'badge badge-' + activity.type">{{ activity.typeLabel }}</span>
</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
</div>
</div>
<script setup>
// 统计数据
totalUsers = 1248;
activeProjects = 86;
pendingTasks = 342;
completionRate = 78;
// 增长率
userGrowthRate = 12.5;
projectGrowthRate = 8.3;
taskChangeRate = 5.2;
completionRateChange = 3.7;
// 当前日期
currentDate = new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
// 最近活动数据
recentActivities = [
{
icon: 'plus-circle',
title: '新用户 "张小明" 注册',
type: 'primary',
typeLabel: '用户',
time: '10分钟前'
},
{
icon: 'folder-plus',
title: '新项目 "电商平台重构" 创建',
type: 'success',
typeLabel: '项目',
time: '35分钟前'
},
{
icon: 'check-circle',
title: '项目 "CRM系统升级" 完成',
type: 'success',
typeLabel: '项目',
time: '2小时前'
},
{
icon: 'exclamation-triangle',
title: '任务 "用户权限模块" 逾期',
type: 'warning',
typeLabel: '任务',
time: '5小时前'
},
{
icon: 'person-plus',
title: '团队成员 "李华" 加入项目',
type: 'primary',
typeLabel: '团队',
time: '昨天'
}
];
// 图表数据
userGrowthData = {
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月'],
values: [820, 890, 920, 950, 1020, 1120, 1248]
};
projectStatusData = [
{value: 32, name: '进行中'},
{value: 24, name: '已完成'},
{value: 18, name: '规划中'},
{value: 12, name: '已暂停'}
];
userActivityData = {
days: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
activeUsers: [420, 532, 601, 534, 630, 410, 320],
newUsers: [120, 132, 101, 134, 90, 60, 40]
};
taskCompletionData = {
weeks: ['第1周', '第2周', '第3周', '第4周'],
completed: [120, 132, 101, 134],
pending: [80, 60, 90, 70]
};
</script>
<script>
// 初始化图表
function initCharts() {
// 用户增长趋势图
const userGrowthChart = echarts.init(document.getElementById('userGrowthChart'));
userGrowthChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: $data.userGrowthData.months,
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
},
splitLine: {
lineStyle: {
color: '#e5e7eb'
}
}
},
series: [{
data: $data.userGrowthData.values,
type: 'line',
smooth: true,
lineStyle: {
width: 4,
color: '#3b82f6'
},
itemStyle: {
color: '#3b82f6',
borderWidth: 2
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(59, 130, 246, 0.5)'
},
{
offset: 1,
color: 'rgba(59, 130, 246, 0.1)'
}
])
}
}]
});
// 项目状态分布图
const projectStatusChart = echarts.init(document.getElementById('projectStatusChart'));
projectStatusChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: {
color: '#6b7280'
}
},
series: [
{
name: '项目状态',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: $data.projectStatusData.map((item, index) => ({
value: item.value,
name: item.name,
itemStyle: {
color: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6'][index]
}
}))
}
]
});
// 用户活跃度图
const userActivityChart = echarts.init(document.getElementById('userActivityChart'));
userActivityChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: ['活跃用户', '新增用户'],
textStyle: {
color: '#6b7280'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: $data.userActivityData.days,
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
}
}
],
yAxis: [
{
type: 'value',
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
},
splitLine: {
lineStyle: {
color: '#e5e7eb'
}
}
}
],
series: [
{
name: '活跃用户',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(59, 130, 246, 0.8)'
},
{
offset: 1,
color: 'rgba(59, 130, 246, 0.1)'
}
])
},
emphasis: {
focus: 'series'
},
data: $data.userActivityData.activeUsers
},
{
name: '新增用户',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(16, 185, 129, 0.8)'
},
{
offset: 1,
color: 'rgba(16, 185, 129, 0.1)'
}
])
},
emphasis: {
focus: 'series'
},
data: $data.userActivityData.newUsers
}
]
});
// 任务完成情况图
const taskCompletionChart = echarts.init(document.getElementById('taskCompletionChart'));
taskCompletionChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['已完成', '待处理'],
textStyle: {
color: '#6b7280'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: $data.taskCompletionData.weeks,
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: '#9ca3af'
}
},
axisLabel: {
color: '#6b7280'
},
splitLine: {
lineStyle: {
color: '#e5e7eb'
}
}
},
series: [
{
name: '已完成',
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
},
data: $data.taskCompletionData.completed,
itemStyle: {
color: '#10b981'
}
},
{
name: '待处理',
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
},
data: $data.taskCompletionData.pending,
itemStyle: {
color: '#f59e0b'
}
}
]
});
// 窗口大小变化时重新调整图表大小
window.addEventListener('resize', function () {
userGrowthChart.resize();
projectStatusChart.resize();
userActivityChart.resize();
taskCompletionChart.resize();
});
}
// 页面加载完成后初始化图表
initCharts()
</script>

@ -6,7 +6,7 @@
*/
const routes = [
{ path: '/', component: '/page/index.html', name: 'home', layout: 'default', meta: { auth: true } },
{ path: '/', component: '/page/index.html', name: 'home', layout: 'public' },
{ path: '/login', component: '/page/login.html', name: 'login', meta: { auth: false } },
{ path: '/profile', component: '/page/profile.html', layout: 'default', meta: { auth: true } },
{ path: '/app', component: '/page/app.html', name: 'app', layout: 'default', meta: { auth: true } },

@ -11,6 +11,20 @@ class TokenService {
this.tokenKey = 'access';
this.refreshTokenKey = 'refresh';
}
check(domain, id, level) {
let body = this.body();
if (!body || !body.access) {
return false;
}
for (let a of body.access) {
if (a.name === domain && (a.tid === '' || a.tid === id) && a.level >= level) {
return true
}
}
return false
}
setBaseUrl(url) {
this.#url = url;
}

Loading…
Cancel
Save