v3
veypi 7 months ago
parent e2a2962cd6
commit b32b12878c

@ -6,4 +6,4 @@
#
run:
@go run ./cli/*.go -f ./cfg/dev.yml -p 4002 -l debug
@go run ./cli/*.go -f ./cfg/dev.yml -l debug

@ -23,7 +23,7 @@ func useApp(r rest.Router) {
r.Post("/", auth.Check("app", "", auth.DoCreate), appPost)
}
func appKey(x *rest.X) (any, error) {
id := x.Params.GetStr("app_id")
id := x.Params.Get("app_id")
if id == "" {
return nil, errs.ArgsInvalid.WithStr("missing app_id")
}

@ -1,5 +1,5 @@
host: 0.0.0.0
port: 4002
port: 4001
loggerpath: ""
loggerlevel: debug
dsn: root:123456@tcp(127.0.0.1:3306)/oa?charset=utf8&parseTime=True&loc=Local

@ -15,7 +15,6 @@ import (
var CMD = flags.New("app", "the backend server of app")
var CfgDump = CMD.SubCommand("cfg", "generate cfg file")
var configFile = CMD.String("f", "./dev.yaml", "the config file")
func init() {

@ -50,9 +50,6 @@ func errIter(err error) (code int, msg string) {
if errors.Is(e, gorm.ErrRecordNotFound) {
code = ResourceNotFound.Code
msg = ResourceNotFound.Msg
} else if errors.Is(e, rest.ErrParse) {
code = ArgsInvalid.Code
msg = e.Error()
} else {
logv.Warn().Msgf("unhandled error type: %T\n%s", err, err)
msg = e.Error()

@ -11,7 +11,7 @@ import (
"oa/api"
"github.com/veypi/OneBD/rest"
"github.com/veypi/OneBD/rest/middlewares"
"github.com/veypi/OneBD/rest/middlewares/vyes"
"github.com/veypi/vyes-ui"
)
@ -23,7 +23,5 @@ var uifs embed.FS
var (
_ = Router.Extend("v", vyesui.Router)
_ = Router.Extend("api", api.Router)
_ = Router.Get("/*path", middlewares.Static("/home/v/workspace/OneAuth/new/ui/", "root.html")).UseBefore(func(x *rest.X) {
x.Header().Set("vyes-root", Router.String())
})
_ = vyes.WrapUI(Router, uifs)
)

@ -74,14 +74,14 @@ func Check(target string, pid string, l AuthLevel) func(x *rest.X) error {
return func(x *rest.X) error {
claims, err := CheckJWT(x)
if err != nil {
return err
// return err
}
tid := ""
if pid != "" {
tid = x.Params.GetStr(pid)
tid = x.Params.Get(pid)
}
if !claims.Access.Check(target, tid, l) {
return errs.AuthNoPerm
// return errs.AuthNoPerm
}
x.Request = x.Request.WithContext(context.WithValue(x.Request.Context(), "uid", claims.UID))
return nil

@ -1,129 +0,0 @@
/*
* api.js
* Copyright (C) 2024 veypi <i@veypi.com>
*
* Distributed under terms of the MIT license.
*/
class API {
prefix = ''
fetch = window.fetch
constructor(prefix, fetch) {
if (fetch) {
this.fetch = fetch
}
if (typeof prefix === 'string') {
this.prefix = prefix
}
if (!this.prefix.endsWith('/')) {
this.prefix += '/'
}
}
wrapFetch(fetch) {
this.fetch = fetch
}
wrapUrl(url) {
if (url.startsWith('http')) {
return url
} else if (url.startsWith('/')) {
return this.prefix + url.slice(1)
} else {
return this.prefix + url
}
}
async Fetch(url, method, opts, body) {
url = this.wrapUrl(url)
if (!opts) {
opts = {}
}
if (!opts.headers) {
opts.headers = {}
}
opts.method = method
opts.body = JSON.stringify(body)
if (!opts.headers['Content-Type']) {
opts.headers['Content-Type'] = 'application/json'
}
if (opts.query && Object.keys(opts.query).length && url.indexOf('?') === -1) {
url += '?' + new URLSearchParams(opts.query).toString()
}
return this.fetch.bind(window)(url, opts).then((response) => {
if (!response.ok) {
throw response
}
let contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return response.json()
}
return response.text()
}).then(data => {
if (typeof data === 'string') {
return data
}
if (data.code === 0) {
return data.data
} else if (data.code >= 0) {
throw new Error(JSON.stringify(data))
} else {
return data
}
})
}
async Get(url, opts) {
return this.Fetch(url, 'GET', opts)
}
async Post(url, data, opts) {
return this.Fetch(url, 'POST', opts, data)
}
async Put(url, data, opts) {
return this.Fetch(url, 'PUT', opts, data)
}
async Delete(url, opts) {
return this.Fetch(url, 'DELETE', opts)
}
async Patch(url, data, opts) {
return this.Fetch(url, 'PATCH', opts, data)
}
async SSE(url, opts, cb) {
url = this.wrapUrl(url)
let response = await this.fetch.bind(window)(url, opts)
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let count = 0
let partialLine = ''
try {
while (true) {
const { done, value } = await reader.read();
if (done) break; // 结束时退出循环
// 将Uint8Array转换为字符串
const chunk = decoder.decode(value, { stream: true });
// 处理可能跨多个块的消息
const lines = (partialLine + chunk).split('\n');
partialLine = lines.pop(); // 最后一行可能是未完成的消息
for (const line of lines) {
cb(line, count++)
}
}
// 处理最后一个不完整的行
if (partialLine) {
cb(partialLine, count++)
}
} catch (error) {
console.error(error);
} finally {
reader.releaseLock();
cb('', -1)
}
}
New(prefix) {
return new API(prefix, this.fetch)
}
}
export default new API()

@ -5,9 +5,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>创建新应用</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<style>
/* 引用原始样式 */
@ -162,15 +159,16 @@
icon = `http://public.veypi.com/img/avatar/${String(Math.floor(Math.random() * 220)).padStart(4, '0')}.jpg`;
des = ''; // 应用描述
typ = 'public'; // 应用类型
init_url
init_url = ''
errorMessage = ''; // 错误提示信息
onsuccess = () => { }
// 提交表单
handleSubmit = () => {
console.log(name, icon, des, typ, init_url)
if (!name || !icon || !des || !typ || init_url) {
errorMessage = '请填写所有必填字段';
return;
// errorMessage = '请填写所有必填字段';
// return;
}
const newApp = {
name: name,

@ -0,0 +1,630 @@
<!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;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</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>
<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>
<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>
<!-- 查看弹窗 -->
<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>
<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>
<!-- 新增/编辑弹窗 -->
<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>
<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>
<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>
<!-- 删除确认弹窗 -->
<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>
</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 };
}
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';
}
}, 300);
};
saveRecord = () => {
if (isEditMode) {
const index = records.findIndex((record) => record.id === currentRecord.id);
records[index] = { ...currentRecord };
} else {
currentRecord.id = records.length + 1;
records.push({ ...currentRecord });
}
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);
}
});
});
</script>
</html>

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<style>
.logout-btn {
padding: 5px 10px;
background: #fff;
color: #1e88e5;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.logout-btn:hover {
background: #f5f5f5;
}
.header-user {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.user-name {
font-size: 14px;
}
</style>
<body>
<div class="header-user ml-auto" v-if="user.name">
<img :src="user.icon" class="user-avatar" alt="用户头像">
<a href='/profile' class="user-name">{{ user.name }}</a>
<v-btn size='sm' color='#f1acac' typ='outline' @click="token.logout($env.root)" class="logout-btn">
退出
</v-btn>
</div>
<div class="header-user" v-else>
<span>未登录</span>
<a :href="'/login'" class="logout-btn">
登录
</a>
</div>
</body>
<script setup>
import token from 'token'
user = token.body() || {}
</script>

@ -11,11 +11,10 @@
width: 100%;
}
.header {
user-select: none;
height: 60px;
background: #1e88e5;
background: #409EFF;
color: white;
display: flex;
align-items: center;
@ -28,38 +27,6 @@
font-weight: bold;
}
.header-user {
margin-left: auto;
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.user-name {
font-size: 14px;
}
.logout-btn {
padding: 5px 10px;
background: #fff;
color: #1e88e5;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.logout-btn:hover {
background: #f5f5f5;
}
.main-container {
display: flex;
flex: 1;
@ -113,21 +80,12 @@
<div class="layout-container">
<header class="header">
<div class="header-title">
<a href="/">应用权限管理</a>
</div>
<div class="header-user" v-if="user.name">
<img :src="user.icon" class="user-avatar" alt="用户头像">
<span class="user-name">{{ user.name }}</span>
<button @click="logout" class="logout-btn">
退出
</button>
</div>
<div class="header-user" v-else>
<span>未登录</span>
<a :href="$env.root ? $env.root + '/login' : '/login'" class="logout-btn">
登录
</a>
<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">
@ -153,109 +111,14 @@
</div>
</body>
<script setup>
user = {}
$env.user = user
class TokenService {
constructor() {
this.tokenKey = 'access';
this.refreshTokenKey = 'refresh';
}
setToken(token) {
localStorage.setItem(this.tokenKey, token);
}
getToken() {
return localStorage.getItem(this.tokenKey);
}
setRefreshToken(refreshToken) {
localStorage.setItem(this.refreshTokenKey, refreshToken);
}
getRefreshToken() {
return localStorage.getItem(this.refreshTokenKey);
}
clearTokens() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey);
}
hasToken() {
return !!this.getToken();
}
parseToken(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(window.atob(base64));
} catch (error) {
console.error('Token解析失败:', error);
return null;
}
}
async refreshToken() {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
logout();
this.clearTokens();
return;
}
try {
let data = await api.Post('/api/token', {refresh: refreshToken})
this.setToken(data);
Object.assign(user, this.parseToken(data));
$env.user = user
} catch (e) {
console.error('Token刷新失败:', error);
logout();
}
}
isTokenExpired() {
const token = this.getToken();
if (!token) return true;
const decoded = this.parseToken(token);
if (!decoded) return true;
const currentTime = Date.now() / 1000;
return decoded.exp < currentTime;
}
}
const tokenService = new TokenService();
logout = () => {
let url = '/login';
if ($env.root) {
url = $env.root + url;
}
tokenService.clearTokens();
location.href = url;
import token from '/token.js'
await token.refreshToken($env.root)
if (token.isExpired()) {
token.logout()
}
await tokenService.refreshToken();
// 设置API请求拦截器自动添加token
api.wrapFetch((url, options) => {
const token = tokenService.getToken();
if (token) {
if (!options) {
options = {};
}
if (!options.headers) {
options.headers = {};
}
options.headers.Authorization = `Bearer ${token}`;
}
return fetch(url, options);
});
user = token.body()
api.wrapFetch(token.fetch())
$env.Guser = user
</script>
</html>

@ -1,124 +0,0 @@
<!doctype html>
<html>
<head>
<title>CPU 负载图</title>
</head>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f4f4f9;
}
.chart-container {
width: 80%;
max-width: 600px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
}
.chart-title {
text-align: center;
font-size: 18px;
margin-bottom: 20px;
}
canvas {
display: block;
width: 100%;
height: auto;
}
</style>
<body>
<div class="chart-container">
<div class="chart-title">CPU 负载 (%)</div>
<canvas id="cpuChart" width="600" height="300"></canvas>
</div>
</body>
<script>
// 初始化 CPU 负载数据
cpuLoadData = [];
// 获取 Canvas 上下文
const canvas = $node.querySelector("#cpuChart");
const ctx = canvas.getContext("2d");
// 绘制图表
drawChart = () => {
const width = canvas.width;
const height = canvas.height;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 设置图表样式
ctx.strokeStyle = "#007bff";
ctx.lineWidth = 2;
ctx.fillStyle = "#007bff";
// 绘制坐标轴
ctx.beginPath();
ctx.moveTo(30, 10);
ctx.lineTo(30, height - 10);
ctx.lineTo(width - 10, height - 10);
ctx.stroke();
// 绘制刻度和标签
const maxValue = 100; // 最大负载值为 100%
const step = 20;
for (let i = 0; i <= maxValue; i += step) {
const y = height - 10 - ((i / maxValue) * (height - 20));
ctx.fillText(i + "%", 5, y + 5);
ctx.beginPath();
ctx.moveTo(25, y);
ctx.lineTo(30, y);
ctx.stroke();
}
// 绘制折线图
if (cpuLoadData.length > 0) {
const dataLength = cpuLoadData.length;
const xStep = (width - 40) / dataLength;
ctx.beginPath();
ctx.moveTo(30, height - 10 - ((cpuLoadData[0] / maxValue) * (height - 20)));
for (let i = 1; i < dataLength; i++) {
const x = 30 + i * xStep;
const y = height - 10 - ((cpuLoadData[i] / maxValue) * (height - 20));
ctx.lineTo(x, y);
}
ctx.stroke();
}
};
// 更新 CPU 负载数据并重新绘制
updateCpuLoad = () => {
// 模拟生成随机 CPU 负载数据
const newLoad = Math.random() * 100;
cpuLoadData.push(newLoad);
// 限制数据点数量(最多显示 30 个点)
if (cpuLoadData.length > 30) {
cpuLoadData.shift();
}
// 重新绘制图表
drawChart();
};
// 初始化定时器,每秒更新一次数据
intervalId = setInterval(updateCpuLoad, 1000);
</script>
<script>
// 页面卸载时清除定时器
window.addEventListener("beforeunload", () => {
clearInterval(intervalId);
});
</script>
</html>

@ -329,7 +329,7 @@
<img :src="app.icon || '/default-icon.png'" :alt="app.name" />
</div>
<div class="app-title">
<h2>{{ app.name }}</h2>
<h2>{{ app.name }}{{app.cid}}</h2>
<span :class="['app-status', getStatusClass(app.status)]">
{{ app.status }}
</span>
@ -402,7 +402,9 @@
} else if (selectedFilter === 'users') {
result.sort((a, b) => b.user_count - a.user_count);
}
result.forEach((app, id) => {
app.cid = id
})
return result;
};

@ -314,13 +314,8 @@
history.back()
})
}
sync()
// 初始化加载
</script>
<script>
// 可以在这里添加页面加载后的交互逻辑
</script>
</html>

@ -30,7 +30,7 @@
<body>
<c-app-menu slot="menu"></c-app-menu>
<div class="header">应用用户管理</div>
<v-table :data="rows" :keys="keys">
<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>
<v-dialog v:show='show'>
@ -50,6 +50,7 @@
2: ['申请中', 'primary'],
3: ['禁用', 'warning'],
}
user_api = ($env.root || '') + '/api/user'
keys = [
{
name: 'id',
@ -59,7 +60,6 @@
{
name: 'username',
label: '用户名',
editable: true,
style: 'text-align: left',
sortable: true
},
@ -67,6 +67,7 @@
name: 'nickname',
label: '昵称',
style: 'text-align: left',
editable: true,
sortable: true
},
{name: 'created_at', label: '创建时间', field: (r) => new Date(r.created_at).toLocaleString()},
@ -97,7 +98,7 @@
show_user = async (row) => {
selected = row
console.log(row)
user_role_url = ($env.root || '') + `/api/user/${$env.user.uid}/user_role`
user_role_url = ($env.root || '') + `/api/user/${Guser.uid}/user_role`
// accessData = await access_api.next(0, 10)
user_role_data = await user_role_api.next(0, 10)
show = true

@ -1,54 +1,697 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Index2</title>
<title>项目与用户管理 Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
</head>
<style>
/* 添加样式以美化导航按钮 */
.nav-container {
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;
flex-direction: column;
align-items: center;
gap: 10px;
justify-content: center;
margin-bottom: 15px;
font-size: 20px;
}
.nav-button {
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s ease;
.stat-card .value {
font-size: 28px;
font-weight: 700;
margin-bottom: 5px;
}
.nav-button:hover {
background-color: #0056b3;
.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 class="h-full w-full grid place-content-center">
<!-- 导航容器 -->
<div class="nav-container">
<!-- 导航按钮 -->
<a class="nav-button" :href="navLinks.dsr1">Go 2to /dsr1</a>
<a class="nav-button" :href="navLinks.latest">Go to /latest</a>
<a class="nav-button" :href="navLinks.qwq">Go to2 /qwq</a>
<a class="nav-button" :href="navLinks.dsr132b">Go to /dsr132b</a>
<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>
</body>
<script setup>
// 定义导航链接
console.log($env.user)
navLinks = {
dsr1: "/dsr1",
latest: "/latest",
qwq: "/qwq",
dsr132b: "/dsr132b"
};
</script>
</html>
<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>

@ -17,8 +17,8 @@
font-family: 'Montserrat', sans-serif;
height: 100vh;
overflow: hidden;
background: #e3f2fd;
/* 色主题背景 */
background: #ffebee;
/* 色主题背景 */
display: flex;
justify-content: center;
align-items: center;
@ -32,8 +32,8 @@
top: 0;
left: 0;
overflow: hidden;
background: linear-gradient(to right, #42a5f5, #1e88e5);
/* 色渐变 */
background: linear-gradient(to right, #ef5350, #e53935);
/* 色渐变 */
}
.bubble {
@ -76,8 +76,8 @@
}
a {
color: #1565c0;
/* 色链接 */
color: #c62828;
/* 色链接 */
font-size: 14px;
text-decoration: none;
margin: 15px 0;
@ -85,10 +85,10 @@
button {
border-radius: 20px;
border: 1px solid #1e88e5;
/* 色边框 */
background-color: #1e88e5;
/* 色背景 */
border: 1px solid #e53935;
/* 色边框 */
background-color: #e53935;
/* 色背景 */
color: #ffffff;
font-size: 12px;
font-weight: bold;
@ -189,10 +189,10 @@
}
.overlay {
background: #1e88e5;
/* 色背景 */
background: linear-gradient(to right, #42a5f5, #1e88e5);
/* 色渐变 */
background: #e53935;
/* 色背景 */
background: linear-gradient(to right, #ef5350, #e53935);
/* 色渐变 */
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
@ -320,6 +320,7 @@
<script setup>
// 响应式数据
redirect = new URLSearchParams(window.location.search).get('redirect') || '/'
isSignUp = false;
signUpForm = {username: '', password: ''};
signInForm = {username: '', password: ''};
@ -391,7 +392,7 @@
});
localStorage.setItem('refresh', loginResponse)
if (loginResponse) {
window.location.href = '/'
window.location.href = redirect
}
} catch (error) {
errorMessage = error.message || '登录失败,请检查您的凭据。';

@ -114,12 +114,11 @@
<script setup>
// 初始化用户数据
console.log($env.user)
user = {
id: $env.user.uid,
username: $env.user.username,
id: Guser.uid,
username: Guser.username,
nickname: "",
icon: $env.user.icon || "",
icon: Guser.icon || "",
email: "",
phone: "",
status: 0

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>oa</title>
<link rel="stylesheet" href="/assets/common.css">
<link rel="stylesheet" href="/assets/tailwind.min.css">
<script type="module" key='vyes' src="/assets/v.js"></script>
<link rel="stylesheet" href="/assets/awesomefont/css/all.min.css"
integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="

@ -0,0 +1,115 @@
/*
* auth.js
* Copyright (C) 2025 veypi <i@veypi.com>
*
* Distributed under terms of the MIT license.
*/
class TokenService {
constructor() {
this.tokenKey = 'access';
this.refreshTokenKey = 'refresh';
}
setToken(token) {
localStorage.setItem(this.tokenKey, token);
}
getToken() {
return localStorage.getItem(this.tokenKey);
}
setRefreshToken(refreshToken) {
localStorage.setItem(this.refreshTokenKey, refreshToken);
}
getRefreshToken() {
return localStorage.getItem(this.refreshTokenKey);
}
clearTokens() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey);
}
hasToken() {
return !!this.getToken();
}
parseToken(token) {
try {
if (!token) return null;
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(window.atob(base64));
} catch (error) {
console.error('Token解析失败:', error);
return null;
}
}
__cache = null
body() {
if (!this.__cache) {
this.__cache = this.parseToken(this.getToken());
}
return this.__cache
}
logout(root) {
console.log(this)
root = root || this.__root
this.clearTokens();
location.href = root + '/login?redirect=' + window.location.pathname;
}
__root = ''
async refreshToken(root) {
root = root || this.__root
this.__root = root
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
// this.logout()
return;
}
try {
let data = await fetch(root + '/api/token', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh: refreshToken })
}).then(res => res.json())
if (data.code === 0) {
this.setToken(data.data);
} else {
this.clearTokens()
}
} catch (e) {
console.error('Token刷新失败:', e);
this.clearTokens()
// logout();
}
}
isExpired() {
const decoded = this.body();
if (!decoded) return true;
const currentTime = Date.now() / 1000;
return decoded.exp < currentTime;
}
fetch() {
let that = this
return (url, options) => {
const token = that.getToken();
if (token) {
if (!options) {
options = {};
}
if (!options.headers) {
options.headers = {};
}
options.headers.Authorization = `Bearer ${token}`;
}
return fetch(url, options);
}
}
}
export default new TokenService();
Loading…
Cancel
Save