mirror of https://github.com/veypi/OneAuth.git
update
parent
e2a2962cd6
commit
b32b12878c
@ -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()
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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…
Reference in New Issue