remove old

v3
veypi 4 months ago
parent c0914bb802
commit 5654a7cd37

@ -1,11 +1,3 @@
# VBase
基于 vhtml/vigo 框架实现,提供用户认证、数据库存储、文件存储等功能。
## 测试
```bash
//重置数据库
go run cli/main.go db drop && go run cli/main.go db migrate
go run cli/main.go -p 4000
```

@ -3,6 +3,18 @@
## 注意
如果开发中发现什么开发规则或者技巧,你可以更新在这个文档,供其他人看
如果下面某些端口或者路径访问失败,请询问用户是否启动应用
## 后端开发
### 测试
```bash
//重置数据库
go run cli/main.go db drop && go run cli/main.go db migrate
// 运行, 可以通过 http://localhost:4000/_api.json 查看接口列表
go run cli/main.go -p 4000
```
## UI 界面开发指南

@ -0,0 +1,166 @@
# VBase 前端界面设计文档
本文档基于 `vigo`/`vhtml` 框架,根据后端设计 (`docs/design.md`) 规划前端界面架构与组件设计。
## 1. 目录结构规范
遵循 `vhtml` 框架约定,建议采用以下目录结构:
```
/ui/
├── assets/ # 静态资源
│ ├── global.css # 全局样式 (Color vars, Reset)
│ └── ...
├── c/ # 业务特定组件 (尽量复用 vhtml-ui仅存放业务强相关组件)
├── layout/ # 页面布局及局部组件
│ ├── default.html # 后台管理布局 (Sidebar + Header)
│ ├── public.html # 公共布局 (Login/Register, Center aligned)
│ ├── header.html # <layout-header>
│ └── sidebar.html # <layout-sidebar>
├── page/ # 页面路由组件
│ ├── auth/ # 认证模块
│ │ ├── login.html
│ │ └── register.html
│ ├── dashboard/ # 仪表盘
│ │ └── index.html
│ ├── sys/ # 系统管理 (User, Org, OAuth)
│ │ ├── user/
│ │ │ ├── index.html # 用户列表
│ │ │ └── form.html # 用户创建/编辑
│ │ ├── org/
│ │ │ ├── index.html # 组织列表
│ │ │ └── detail.html # 组织详情/设置
│ │ └── oauth/
│ │ ├── index.html # 应用列表
│ │ └── form.html # 应用注册
│ ├── user/ # 个人中心
│ │ └── profile.html
│ ├── 404.html
│ └── 403.html
├── env.js # 全局环境配置 (Axios, Router guard)
├── routes.js # 路由定义
└── root.html # 根入口
```
## 2. 布局设计 (Layouts)
### 2.1 Public Layout (`layout/public.html`)
- **用途**: 登录、注册、404、403 等无需鉴权的页面。
- **结构**: 居中容器,背景简约,包含 Logo 和主体内容插槽。
### 2.2 Default Layout (`layout/default.html`)
- **用途**: 系统核心业务页面。
- **结构**:
- **Sidebar (Left)**: 导航菜单 (Dashboard, User, Org, OAuth, Settings)。支持折叠。
- **Header (Top)**: 面包屑导航, 组织切换器 (Org Switcher), 用户头像/下拉菜单 (Profile, Logout)。
- **Main Content**: 路由视图插槽 (`<router-view>`)。
- **特性**: 需集成 `OrgID` 切换逻辑,切换组织时更新全局状态并刷新数据。
## 3. 核心模块界面规划与权限
### 3.1 认证模块 (Auth)
- **权限**: 公开访问 (Guest)
- **登录页 (`/login`)**: 用户名/邮箱 + 密码。集成 OAuth2 第三方登录按钮。
- **注册页 (`/register`)**: 基本信息填写。
### 3.2 仪表盘 (Dashboard)
- **权限**: 登录用户 (Authenticated)
- **概览页 (`/`)**: 展示核心指标。
- **普通用户**: 仅可见个人或所属项目的数据概览。
- **管理员**: 可见整个组织或系统的宏观数据。
### 3.3 组织管理 (Org Module)
- **组织列表 (`/org`)**:
- **权限**: 登录用户 (Authenticated)
- 展示用户加入的所有组织。
- “创建组织”入口:视系统配置开放给所有用户或仅限特定用户。
- **组织详情/设置 (`/org/:id`)**:
- **权限**: 组织成员 (Member of Org)
- **基本信息**: 仅 **Org Admin** 可修改名称、Logo。
- **成员管理**: 仅 **Org Admin** 可邀请、移除成员或修改角色。
- **只读视图**: 普通成员仅可查看组织信息和成员列表。
### 3.4 用户管理 (User Module)
- **用户列表 (`/sys/user`)**:
- **权限**: 系统管理员 (System Admin Only)
- 全局用户管理表格,包含搜索、分页、禁用/启用用户功能。
- **个人资料 (`/profile`)**:
- **权限**: 登录用户 (Authenticated)
- 基本信息修改 (头像, 昵称)、账号安全 (修改密码, 绑定第三方账号)。
### 3.5 OAuth2 Provider (OAuth Module)
- **应用管理 (`/sys/oauth`)**:
- **权限**: 登录用户 (Authenticated)
- 开发者注册的 OAuth 应用列表。
- **创建/编辑应用**: 用户仅可管理自己创建的应用。
- **系统级应用**: 仅 **System Admin** 可见或管理系统预置应用。
## 4. 组件策略
### 4.1 基础组件库
本项目直接使用 **vhtml-ui** 组件库,无需重复封装基础组件。
- 按钮、输入框、卡片、模态框、表格等均直接使用库组件。
- 组件文档:`http://localhost:4000/v/README.md`
### 4.2 业务组件
- 仅当组件包含特定业务逻辑且多处复用时,封装为自定义组件。
- 命名规范:使用短前缀(如 `c-` 或直接目录名)。
- 存放位置:
- 全局复用:`/ui/c/`
- 局部复用:直接存放在调用方同级目录或 `_components` 子目录中。
## 5. 路由规划 (`routes.js`)
```javascript
const routes = [
// Public
{ path: '/login', component: '/page/auth/login.html', layout: 'public', meta: { guest: true } },
{ path: '/register', component: '/page/auth/register.html', layout: 'public', meta: { guest: true } },
// Dashboard (Default Layout)
{
path: '/',
layout: 'default',
meta: { auth: true }, // 登录用户均可访问
component: '/page/dashboard/index.html'
},
// Org Management
{ path: '/org', component: '/page/sys/org/index.html', layout: 'default', meta: { auth: true } },
{
path: '/org/:id',
component: '/page/sys/org/detail.html',
layout: 'default',
meta: { auth: true } // 页面内部需校验是否为成员
},
// User System
{ path: '/profile', component: '/page/user/profile.html', layout: 'default', meta: { auth: true } },
{
path: '/users',
component: '/page/sys/user/index.html',
layout: 'default',
meta: { auth: true, roles: ['admin'] } // 仅系统管理员
},
// OAuth Management
{ path: '/oauth/apps', component: '/page/sys/oauth/index.html', layout: 'default', meta: { auth: true } },
// Errors
{ path: '/403', component: '/page/403.html', layout: 'public' },
{ path: '*', component: '/page/404.html', layout: 'public' }
]
```
## 6. 交互与状态管理
- **全局状态 (`$env.$G`)**:
- `user`: 当前登录用户信息。
- `currentOrg`: 当前选中的组织上下文 (影响 Header 显示和 API 请求 Header)。
- `token`: JWT Token 管理。
- **API 请求**:
- 使用 `$axios` 拦截器,自动注入 `Authorization: Bearer ...`
- 自动注入 `X-Org-ID` (当 `currentOrg` 存在时)。
- **权限控制**:
- 路由守卫 (`beforeEnter`) 检查登录状态和角色。
- 页面内使用 `v-if` 或指令控制按钮级别的权限显示。

@ -7,19 +7,30 @@
package vbase
import (
"embed"
"github.com/veypi/vbase/api"
"github.com/veypi/vbase/auth"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vhtml"
vhtmlui "github.com/veypi/vhtml-ui"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()
var Auth = auth.Factory
var Config = cfg.Config
var AuthMiddleware = auth.AuthMiddleware
var (
Auth = auth.Factory
Config = cfg.Config
AuthMiddleware = auth.AuthMiddleware
)
//go:embed ui
var uifs embed.FS
func init() {
// 挂载 API 路由
Router.Extend("/api", api.Router)
Router.Extend("v", vhtmlui.Router)
Router.Extend("vhtml", vhtml.Router)
vhtml.WrapUI(Router, uifs)
}

@ -1,86 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Create App Component">
<title>创建新应用</title>
</head>
<style>
body {
background-color: var(--bg-color-secondary);
color: var(--text-color-primary);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
}
h1 {
text-align: center;
color: var(--color-primary);
margin-bottom: var(--spacing-lg);
font-size: 1.5rem;
}
</style>
<body>
<h1>创建新应用</h1>
<v-form :data="formData" @submit="handleSubmit">
<v-input name="name" label="应用名称" required placeholder="请输入应用名称"></v-input>
<v-input name="icon" label="应用图标URL" required placeholder="请输入应用图标的URL"></v-input>
<v-input name="des" label="应用描述" type="textarea" placeholder="请输入应用描述"></v-input>
<v-input name="typ" label="应用类型" type="select" :opts="typeOpts"></v-input>
<v-input name="init_url" label="应用地址" required placeholder="请输入应用首页"></v-input>
<div class="mt-4">
<v-btn type="submit" block>提交</v-btn>
</div>
</v-form>
</body>
<script setup>
formData = {
name: '',
icon: `http://public.veypi.com/img/avatar/${String(Math.floor(Math.random() * 220)).padStart(4, '0')}.jpg`,
des: '',
typ: 'public',
init_url: ''
}
typeOpts = [
{ label: '公开制', value: 'public' },
{ label: '申请制', value: 'apply' },
{ label: '邀请制', value: 'invite' }
]
onsuccess = () => {}
handleSubmit = () => {
const { name, icon, des, typ, init_url } = formData
if (!name || !icon || !typ || !init_url) {
$message.error('请填写所有必填字段')
return
}
const newApp = {
name,
icon,
des: des || null,
typ,
status: 'ok',
init_url
}
$axios.post('/api/app', newApp)
.then(() => {
$message.success('创建成功')
onsuccess()
// Reset form
formData.name = ''
formData.icon = `http://public.veypi.com/img/avatar/${String(Math.floor(Math.random() * 220)).padStart(4, '0')}.jpg`
formData.des = ''
formData.typ = 'public'
formData.init_url = ''
})
.catch((err) => {
$message.error(err.message || '创建应用失败,请稍后重试')
})
}
</script>
</html>

@ -1,231 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Table Component">
</head>
<style>
body {}
.table-column {
flex-grow: 1;
max-width: 30%;
border-bottom: 2px solid var(--border-color);
background-color: var(--bg-color-secondary);
}
.sticky-column {
position: sticky;
z-index: 10;
}
.header-key {
border-bottom: 2px solid var(--border-color);
padding: 0 var(--spacing-sm);
font-size: 1.1rem;
height: 3rem;
line-height: 3rem;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
box-sizing: border-box;
font-weight: 600;
color: var(--text-color-primary);
}
.table-value {
display: block;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: var(--spacing-sm);
height: 3rem;
line-height: 2rem;
color: var(--text-color-secondary);
}
.table-value[odd='1'] {
background-color: color-mix(in srgb, var(--bg-color-secondary), black 2%);
}
.dialog-content {
min-height: 50vh;
max-height: 80vh;
overflow: auto;
width: 50vw;
background-color: var(--bg-color-secondary);
box-shadow: var(--shadow-lg);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
display: flex;
flex-direction: column;
}
::-webkit-scrollbar {
width: 0.25rem;
height: 0.25rem;
}
::-webkit-scrollbar-track {
background: none;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 5px;
}
.keysearch {
padding: 0.5rem 1rem;
font-size: 0.875rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
outline: none;
transition: var(--transition-base);
background-color: var(--bg-color-primary);
color: var(--text-color-primary);
}
.keysearch:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary), transparent 80%);
}
</style>
<body>
<div class="flex justify-evenly overflow-x-auto">
<div v-if='!key.hidden' class="table-column" :class="{'sticky-column':index===0}" style="min-height: 21rem;left:0"
v-for='(key,index) in keys'>
<div class="header-key">{{key.label||key.name}}</div>
<div class="table-value" :odd='index%2' v-for='(row, index) in data'>
<vslot :name='key.name' v='row,index' :style="key.style">
<div v-if='editable && !key.disabled && row._enable'>
<v-input v:value='row[key.name]' :type="key.type==='textarea'?'text':key.type"
:required='key.required' :opts='key.opts'></v-input>
</div>
<div v-else>
{{ (key.field?key.field(row):row[key.name]) || '&nbsp;'}}
</div>
</vslot>
</div>
</div>
<div class="table-column sticky-column" style="right:0;min-width: 8rem;">
<vslot name='_key' class="header-key">
<v-dropdown :items="[{label:'创建',value:0},{label:'高级检索',value:1},{label:'智能导入',value:2}]" @command="show">
<div class="flex items-center cursor-pointer">
<i class="fas fa-ellipsis-v"></i>
</div>
</v-dropdown>
</vslot>
<div class="table-value" :odd='index%2' v-for='(row, index) in data'>
<vslot class="w-full flex justify-center gap-2 text-xl" name='_addon' v='row,index'>
<i class="fas fa-edit text-blue-500 cursor-pointer" v-if='!row._enable' @click='row._enable=true'></i>
<i class="fas fa-save text-red-500 cursor-pointer" v-else @click='wrap(1, row)'></i>
<i class="fas fa-trash text-red-500 cursor-pointer" v-if='!row._enable' @click='wrap(3, row)'></i>
<i class="fas fa-times text-gray-500 cursor-pointer" v-else @click='delete row._enable'></i>
</vslot>
</div>
</div>
</div>
<div class="flex items-center gap-2 px-4 select-none h-12">
<input !value='listOpts.keyword' @input.delay1s='search' class="keysearch" placeholder="简单检索" />
<i class="fas fa-chevron-left cursor-pointer" @click='wrap(0,-1)'></i>
<div>{{listOpts.page}}</div>
<i class="fas fa-chevron-right cursor-pointer" @click='wrap(0,1)'></i>
<div class="">总计{{total}}条数据</div>
</div>
<v-dialog v:show='showFlag'>
<div class="dialog-content">
<vslot v-if='showMode==0' name='create' v='keys,oncreate'>
<table-create :keys='keys' :oncreate='oncreate'></table-create>
</vslot>
<table-setting :keys='keys' :opts='listOpts' v-else-if='showMode==1' :apply='()=>wrap(0)'>
</table-setting>
<div v-else-if class="w-full flex flex-col flex-grow items-center gap-4" style=" height: calc(100% - 0px);">
<textarea class="w-full bg-gray-200 flex-grow p-4" placeholder="请输入文本内容或者拖入文件" !value='ai_content'
@input='ai_content=$event.target.value' style="resize:vertical;"></textarea>
<v-btn class="mx-auto" size='lg' @click='ai'>智能识别</v-btn>
</div>
</div>
</v-dialog>
</body>
<script setup>
showFlag = false
showMode = 0
loading = false
keys = []
data = []
host = window.location.origin
api = ''
editable = false
ai_content = ''
show = (m) => {
showMode = m.value !== undefined ? m.value : m // Handle v-dropdown event
showFlag = true
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
update = async (row) => {
if (api) {
return await $env.api.Patch(host + api + '/' + row.id, row)
}
}
total = 0
listOpts = {
page: 1,
page_size: 10,
keyword: '',
keywords: {},
sort_by: 'created_at',
order: 'desc',
}
next = async (opts) => {
if (api) {
opts = Object.assign({}, opts)
if (opts.keywords && Object.keys(opts.keywords).length > 0) {
opts.keywords = JSON.stringify(opts.keywords)
} else {
delete opts.keywords
}
return await $axios.get(host + api, opts)
}
return []
}
create = async (data) => {
if (api) {
return await $axios.post(host + api, data)
}
return
}
// ... rest of the logic
// Re-implementing simplified versions of other methods as they were cut off in read
wrap = (mode, data) => {
// Mock wrap function logic
if(mode === 0) {
// Pagination
listOpts.page += data
if(listOpts.page < 1) listOpts.page = 1
// Trigger search/reload
} else if (mode === 1) {
// Save
update(data)
delete data._enable
} else if (mode === 3) {
// Delete
// del(data)
}
}
search = (e) => {
listOpts.keyword = e.target.value
// Trigger search
}
oncreate = () => {
showFlag = false
// Reload data
}
ai = () => {
// AI logic
}
</script>
</html>

@ -1,14 +1,9 @@
import routes from './routes.js'
import token from './token.js'
export default ($env) => {
token.wrapAxios($env.$axios)
$env.$G.token = token
let user = token.body()
$env.$G.user = user
$env.$router.addRoutes(routes)
$env.$router.beforeEnter = async (to, from, next) => {
if (to.meta && to.meta.auth) {
if (token.isExpired()) {

@ -1,123 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Auth Layout</title>
<style>
.layout-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
background-color: var(--bg-color);
color: var(--text-color);
}
.header {
user-select: none;
height: 60px;
background: var(--color-primary);
color: var(--color-primary-text);
display: flex;
align-items: center;
padding: 0 var(--spacing-lg);
box-shadow: var(--shadow-sm);
}
.header-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
}
.header-title a {
color: inherit;
text-decoration: none;
}
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
.menu {
border-right: 1px solid var(--border-color);
background-color: var(--bg-color-secondary);
}
.content {
flex: 1;
overflow-y: auto;
width: 100%;
height: 100%;
padding: var(--spacing-md);
}
.footer {
height: 40px;
background: var(--bg-color-secondary);
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
color: var(--text-color-secondary);
}
.nav-link {
color: var(--color-primary-text);
text-decoration: none;
margin-left: var(--spacing-md);
font-size: var(--font-size-md);
opacity: 0.9;
transition: opacity var(--transition-base);
}
.nav-link:hover {
opacity: 1;
}
</style>
</head>
<body style="height: 100%;width: 100%;margin: 0;">
<div class="layout-container">
<header class="header">
<div class="header-title">
<a href="@/">首页</a>
</div>
<a class="nav-link ml-auto" href="/">应用权限管理</a>
<div vsrc='ico.html' class="ml-auto" style="margin-left: auto;"></div>
</header>
<div class="main-container">
<vslot v='user' class="menu" name='menu'>
<v-sidebar :items="menuItems" width="220px" style="height: 100%;"></v-sidebar>
</vslot>
<vslot class="content">
</vslot>
</div>
<footer class="footer">
Copyright © 2025 veypi. All Rights Reserved.
</footer>
</div>
</body>
<script setup>
id = $router.params.id;
menuItems = [
{ label: "应用简介", path: `/app/${id}` },
{ label: "用户管理", path: `/app/${id}/user` },
{ label: "权限管理", path: `/app/${id}/auth` },
{ label: "应用设置", path: `/app/${id}/settings` }
];
try {
app = await $axios.get(`/api/app/${id}`)
document.title = `${app.name} - 项目主页`
$G.app = app
} catch (e) {
$router.push('/')
console.log(e)
}
</script>
</html>

@ -1,113 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="404 Page Not Found" details="页面未找到">
<title>404 - Galaxy Lost</title>
</head>
<style>
body {
--primary-color: var(--color-primary);
--bg-gradient-start: color-mix(in srgb, var(--bg-color), #000 80%);
--bg-gradient-end: color-mix(in srgb, var(--bg-color), #000 60%);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, var(--bg-gradient-start), var(--bg-gradient-end));
font-family: var(--font-family);
overflow: hidden;
color: #fff;
}
.container {
position: relative;
z-index: 2;
text-align: center;
perspective: 1000px;
}
.error-code {
font-size: 20rem;
font-weight: 900;
margin: 0;
text-shadow: 0 0 30px var(--primary-color);
color: #fff;
transform-style: preserve-3d;
animation: float 4s ease-in-out infinite;
}
.message {
font-size: 2rem;
margin: 2rem 0;
opacity: 0.8;
transform: translateZ(50px);
animation: textGlow 2s alternate infinite;
}
.planet {
position: absolute;
width: 300px;
height: 300px;
border-radius: 50%;
background: linear-gradient(45deg, color-mix(in srgb, var(--primary-color), transparent 20%), color-mix(in srgb, var(--primary-color), #000 40%), #ffaf7b);
filter: drop-shadow(0 0 50px rgba(255, 175, 123, 0.5));
animation: rotate 30s linear infinite;
left: 20%;
top: 30%;
z-index: 1;
}
.meteor {
position: absolute;
width: 2px;
height: 30px;
background: linear-gradient(to bottom, white, var(--primary-color));
animation: meteorFall 3s linear infinite;
z-index: 1;
}
@keyframes float {
0%, 100% { transform: translateY(0) rotateX(10deg) rotateY(10deg); }
50% { transform: translateY(-20px) rotateX(-10deg) rotateY(-10deg); }
}
@keyframes textGlow {
from { text-shadow: 0 0 10px rgba(255, 255, 255, 0.5); }
to { text-shadow: 0 0 20px rgba(255, 255, 255, 0.8), 0 0 30px var(--primary-color); }
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes meteorFall {
0% { transform: translateY(-100vh) translateX(100vw) rotate(45deg); opacity: 1; }
100% { transform: translateY(100vh) translateX(-100vw) rotate(45deg); opacity: 0; }
}
</style>
<body>
<div class="planet"></div>
<!-- Generate some meteors -->
<div class="meteor" style="left: 10%; animation-delay: 0s;"></div>
<div class="meteor" style="left: 30%; animation-delay: 2s;"></div>
<div class="meteor" style="left: 60%; animation-delay: 1s;"></div>
<div class="meteor" style="left: 80%; animation-delay: 3s;"></div>
<div class="container">
<h1 class="error-code">404</h1>
<div class="message">Oops! You seem to be lost in space.</div>
<v-btn :click="goHome" size="lg" variant="primary" round style="font-size: 1.2rem; padding: 0.8rem 3rem;">Go Home</v-btn>
</div>
</body>
<script setup>
goHome = () => {
$router.push('/');
}
</script>
</html>

@ -1,216 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="App List Page">
<title>应用列表</title>
</head>
<style>
body {
padding: var(--spacing-lg);
background-color: var(--bg-color-primary);
color: var(--text-color-primary);
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
gap: var(--spacing-md);
}
h1 {
font-size: 2rem;
font-weight: 700;
color: var(--color-primary);
}
.filters {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--spacing-lg);
}
.app-card {
background-color: var(--bg-color-secondary);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
transition: var(--transition-base);
display: flex;
flex-direction: column;
overflow: hidden;
}
.app-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.app-card-header {
padding: var(--spacing-md);
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.app-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-sm);
margin-right: var(--spacing-md);
object-fit: cover;
background-color: var(--bg-color-tertiary);
}
.app-info {
flex: 1;
}
.app-title {
font-weight: 600;
margin-bottom: var(--spacing-xs);
color: var(--text-color-primary);
}
.app-status {
font-size: 0.8rem;
padding: 2px 8px;
border-radius: 10px;
}
.status-active {
background-color: color-mix(in srgb, var(--color-success), transparent 90%);
color: var(--color-success);
}
.status-pending {
background-color: color-mix(in srgb, var(--color-warning), transparent 90%);
color: var(--color-warning);
}
.status-inactive {
background-color: color-mix(in srgb, var(--color-danger), transparent 90%);
color: var(--color-danger);
}
.app-card-body {
padding: var(--spacing-md);
flex: 1;
color: var(--text-color-secondary);
font-size: 0.9rem;
line-height: 1.5;
}
.app-card-footer {
padding: var(--spacing-md);
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
background-color: color-mix(in srgb, var(--bg-color-secondary), black 2%);
}
</style>
<body layout="app">
<header>
<h1>应用中心</h1>
<div style="width: 300px;">
<v-input v:value="searchQuery" placeholder="搜索应用..." prefix="search"></v-input>
</div>
</header>
<div class="filters">
<v-btn v-for="filter in filters" :key="filter.value"
:variant="currentFilter === filter.value ? 'primary' : 'outline'" @click="currentFilter = filter.value" size="sm">
{{ filter.label }}
</v-btn>
</div>
<div class="app-grid">
<div class="app-card" v-for="app in filteredApps" :key="app.id">
<div class="app-card-header">
<img :src="app.icon" class="app-icon" alt="icon">
<div class="app-info">
<div class="app-title">{{ app.name }}</div>
<span class="app-status" :class="getStatusClass(app.status)">
{{ getStatusLabel(app.status) }}
</span>
</div>
</div>
<div class="app-card-body">
{{ app.des || '暂无描述' }}
</div>
<div class="app-card-footer">
<v-btn size="sm" variant="text" @click="openApp(app)">访问</v-btn>
<v-btn size="sm" variant="outline" @click="manageApp(app)">管理</v-btn>
</div>
</div>
</div>
</body>
<script setup>
searchQuery = ''
currentFilter = 'all'
filters = [
{label: '全部', value: 'all'},
{label: '我的应用', value: 'my'},
{label: '最近使用', value: 'recent'}
]
apps = []
filteredApps = []
getStatusClass = (status) => {
if (status === 'ok') return 'status-active'
if (status === 'pending') return 'status-pending'
return 'status-inactive'
}
getStatusLabel = (status) => {
if (status === 'ok') return '运行中'
if (status === 'pending') return '审核中'
return '已停止'
}
openApp = (app) => {
if (app.init_url) {
window.open(app.init_url, '_blank')
} else {
$message.info('该应用暂无入口')
}
}
manageApp = (app) => {
$router.push(`/app/${app.id}/settings`)
}
// Fetch apps
$axios.get('/api/app').then(res => {
apps = res || []
$data.filteredApps = apps
})
</script>
<script>
$watch(() => [searchQuery, currentFilter, apps], () => {
let result = $data.apps
if ($data.searchQuery) {
const q = $data.searchQuery.toLowerCase()
result = result.filter(app => app.name.toLowerCase().includes(q) || (app.des && app.des.toLowerCase().includes(q)))
}
// Filter logic for 'my' and 'recent' would go here, simplified for now
if ($data.currentFilter === 'my') {
// Mock logic
}
$data.filteredApps = result
})
</script>
</html>

@ -1,161 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>权限管理</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="权限管理" details="管理应用资源、角色及权限分配">
</head>
<style>
body {
padding: var(--spacing-lg);
background-color: var(--bg-color-secondary);
color: var(--text-color);
}
.header {
font-size: 1.5rem;
line-height: 1.5;
margin-bottom: var(--spacing-lg);
font-weight: 600;
color: var(--text-color);
display: flex;
align-items: center;
gap: 0.5rem;
}
.sub-header {
font-size: 1.25rem;
line-height: 1.5;
margin-bottom: var(--spacing-lg);
font-weight: 600;
color: var(--text-color);
border-left: 4px solid var(--color-primary);
padding-left: var(--spacing-md);
}
.dialog {
height: 60vh;
width: 60vw;
background-color: var(--bg-color);
box-shadow: var(--shadow-lg);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
color: var(--text-color);
display: flex;
flex-direction: column;
}
.dialog-title {
font-size: 1.5rem;
margin-bottom: var(--spacing-lg);
font-weight: 600;
flex-shrink: 0;
}
/* Responsive dialog */
@media (max-width: 768px) {
.dialog {
width: 90vw;
height: 80vh;
}
}
</style>
<body>
<div class="header">
<i class="fa-solid fa-shield-halved"></i> 应用权限管理
</div>
<div class="sub-header">资源管理</div>
<v-table :axios='$axios' :onerr="onerr" :keys="keys" :api='resource_url'></v-table>
<div class="sub-header" style="margin-top: var(--spacing-xl);">角色管理</div>
<v-table :axios='$axios' :onerr="onerr" :keys="role_keys" :api='role_url'>
<v-btn vslot='_addon' size='sm' @click='show_user(row)' variant="primary" style="margin-right: 5px;">
<i class="fa-solid fa-list-check"></i> 权限表
</v-btn>
<v-btn v-if='row.id!==$G.app.init_role_id' vslot='_addon' size='sm' @click='update_app({init_role_id: row.id})'
variant="warning">
<i class="fa-solid fa-star"></i> 设为初始角色
</v-btn>
<v-btn v-else vslot='_addon' size='sm' disabled variant="success">
<i class="fa-solid fa-check"></i> 初始角色
</v-btn>
</v-table>
<v-dialog v:show='show'>
<div class="dialog">
<div class="dialog-title">
<i class="fa-solid fa-user-tag"></i> {{selected_role.name}} 权限配置
</div>
<div style="flex: 1; overflow: hidden;">
<v-table :querys='{role_id: selected_role?.id}' :axios='$axios' :onerr="onerr" :keys="access_keys"
:api='access_url' :data="accessData" height="100%"></v-table>
</div>
<div style="margin-top: var(--spacing-md); text-align: right;">
<v-btn @click="show = false">关闭</v-btn>
</div>
</div>
</v-dialog>
</body>
<script setup>
id = $router.params.id
rows = []
// 错误处理函数
onerr = (e) => {
$message.error(e.message || '操作失败');
}
keys = [
{name: 'id', label: 'ID', disabled: true, style: {width: '4rem'}},
{name: 'name', required: true, label: '资源名称'},
{name: 'des', label: '资源描述', editable: true},
{name: 'created_at', label: '创建时间', disabled: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', disabled: true, field: (r) => new Date(r.updated_at).toLocaleString()},
]
role_keys = [
{name: 'id', label: 'ID', disabled: true, style: {width: '4rem'}},
{name: 'name', required: true, label: '角色名称'},
{name: 'des', required: true, label: '角色描述', editable: true},
{name: 'user_count', label: '用户数量', disabled: true},
{name: 'created_at', label: '创建时间', disabled: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', disabled: true, field: (r) => new Date(r.updated_at).toLocaleString()},
]
access_keys = [
{name: 'id', label: 'ID', disabled: true, style: {width: '4rem'}},
{name: 'name', required: true, label: '资源名'},
{name: 'tid', label: '限制域', editable: true},
{name: 'level', label: '权限等级', type: 'number', editable: true},
{name: 'created_at', label: '创建时间', disabled: true, field: (r) => new Date(r.created_at).toLocaleString()},
{name: 'updated_at', label: '更新时间', disabled: true, field: (r) => new Date(r.updated_at).toLocaleString()},
]
show = false
role_user_keys = []
selected_role = {}
accessData = []
user_role_data = []
show_user = async (row) => {
selected_role = row
access_url = `/api/app/${id}/access`
show = true
}
resource_url = `/api/app/${id}/resource`
access_url = ``
role_url = `/api/app/${id}/role`
user_role_url = `/api/user/_/user_role/${selected_role.id}`
update_app = (props) => {
$axios.patch(`/api/app/${id}`, props).then((res) => {
if (res) {
Object.assign($G.app, res)
$message.success('更新成功');
}
}).catch(onerr)
}
</script>
</html>

@ -1,323 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="项目主页" details="展示特定项目的详细信息和统计数据">
<title>项目主页</title>
</head>
<style>
body {
background-color: var(--bg-color-secondary);
color: var(--text-color);
padding: var(--spacing-lg);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
background: linear-gradient(135deg, var(--color-primary), color-mix(in srgb, var(--color-primary), black 20%));
color: white;
padding: 40px 0;
border-radius: var(--radius-lg);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-md);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30px;
}
.project-info {
display: flex;
align-items: center;
}
.project-icon {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 4px solid white;
box-shadow: var(--shadow-sm);
margin-right: 20px;
}
.project-title h1 {
font-size: 2.5rem;
margin-bottom: 5px;
color: white;
}
.project-type {
display: inline-block;
background-color: rgba(255, 255, 255, 0.2);
padding: 5px 15px;
border-radius: 30px;
font-size: 0.9rem;
font-weight: 600;
}
.project-status {
background-color: var(--color-success);
color: white;
padding: 8px 20px;
border-radius: 30px;
font-weight: bold;
display: inline-flex;
align-items: center;
box-shadow: var(--shadow-sm);
}
.project-status i {
margin-right: 8px;
}
.main-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--spacing-xl);
}
.description-card,
.stats-card {
background-color: var(--bg-color);
border-radius: var(--radius-lg);
padding: 30px;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
}
.card-title {
font-size: 1.6rem;
margin-bottom: 20px;
color: var(--color-primary);
border-bottom: 2px solid var(--border-color);
padding-bottom: 10px;
}
.description-content {
font-size: 1.1rem;
line-height: 1.7;
color: var(--text-color);
}
.stat-item {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.stat-icon {
width: 50px;
height: 50px;
background-color: color-mix(in srgb, var(--color-primary), transparent 90%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
color: var(--color-primary);
font-size: 1.4rem;
}
.stat-info h3 {
font-size: 1.3rem;
margin-bottom: 5px;
color: var(--text-color);
}
.stat-info p {
color: var(--text-color-secondary);
}
.date-stat {
display: flex;
justify-content: space-between;
margin-top: 30px;
border-top: 1px solid var(--border-color);
padding-top: 20px;
}
.date-item {
text-align: center;
flex: 1;
}
.date-item:first-child {
border-right: 1px solid var(--border-color);
}
.date-label {
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.date-value {
font-size: 1.1rem;
font-weight: bold;
margin-top: 5px;
color: var(--text-color);
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.header-content {
flex-direction: column;
text-align: center;
}
.project-info {
flex-direction: column;
margin-bottom: 20px;
}
.project-icon {
margin-right: 0;
margin-bottom: 15px;
}
}
</style>
<body>
<div class="container">
<header>
<div class="header-content">
<div class="project-info">
<img :src="app.icon || 'http://public.veypi.com/img/avatar/0075.jpg'" alt="项目图标" class="project-icon">
<div class="project-title">
<h1>{{app.name || '未命名项目'}}</h1>
<span class="project-type"><i class="fas fa-globe"></i> {{app.typ || '公开项目'}}</span>
</div>
</div>
<div class="project-status">
<i class="fas fa-check-circle"></i> {{app.status || '状态良好'}}
</div>
</div>
</header>
<div class="main-content">
<div class="description-card">
<h2 class="card-title">项目描述</h2>
<div class="description-content">
<p v-if="app.des">{{app.des}}</p>
<p v-else>这是一个公开的项目,欢迎查看和参与。项目处于良好状态,您可以安全地使用和贡献。</p>
</div>
</div>
<div class="stats-card">
<h2 class="card-title">项目统计</h2>
<div class="stat-item">
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
<div class="stat-info">
<h3>{{app.user_count || 0}}</h3>
<p>参与用户</p>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">
<i class="fas fa-key"></i>
</div>
<div class="stat-info">
<h3>{{$G.app.id ? $G.app.id.substring(0, 8) : 'N/A'}}</h3>
<p>项目ID (缩略)</p>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">
<i class="fas fa-shield-alt"></i>
</div>
<div class="stat-info">
<h3>{{app.typ || '公开'}}</h3>
<p>项目类型</p>
</div>
</div>
<div class="date-stat">
<div class="date-item">
<div class="date-label">创建于</div>
<div class="date-value">{{formatDate(app.created_at) || 'N/A'}}</div>
</div>
<div class="date-item">
<div class="date-label">最后更新</div>
<div class="date-value">{{formatDate(app.updated_at) || 'N/A'}}</div>
</div>
</div>
</div>
</div>
</div>
</body>
<script setup>
// 默认应用数据
app = {
id: '',
name: '',
icon: '',
des: '',
typ: '',
status: '',
created_at: '',
updated_at: '',
user_count: 0,
init_role: null,
init_url: ''
}
// 格式化日期函数
formatDate = (isoString) => {
if (!isoString) return null;
const date = new Date(isoString);
if (isNaN(date.getTime())) return isoString; // 如果解析失败返回原字符串
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
// 同步应用数据
sync = () => {
// 从路由参数获取ID或者使用默认值/模拟值
const id = $router.params.id || $G.app?.id
if (!id) {
// 如果没有ID可能是直接访问展示示例数据或跳转
// $router.push('/app')
return
}
$axios.get(`/api/app/${id}`)
.then((data) => {
Object.assign(app, data)
document.title = `${app.name} - 项目主页`
})
.catch((e) => {
console.log(e)
// 错误处理,保留在当前页或跳转
// $router.push('/app')
})
}
// 页面加载时执行
sync()
</script>
</html>

@ -1,104 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="应用设置" details="配置应用的基本信息">
<title>应用设置</title>
</head>
<style>
body {
padding: var(--spacing-lg);
background-color: var(--bg-color-secondary);
color: var(--text-color);
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: var(--spacing-lg);
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-color);
}
.settings-section {
background-color: var(--bg-color);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow-md);
margin-bottom: var(--spacing-xl);
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color-secondary);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-md);
margin-top: var(--spacing-xl);
}
</style>
<body>
<div class="settings-section">
<div class="section-title">
<i class="fa-solid fa-gear"></i> 基本设置
</div>
<div class="form-group">
<label class="form-label">应用名称</label>
<v-input v:value="form.name" placeholder="请输入应用名称"></v-input>
</div>
<div class="form-group">
<label class="form-label">应用图标 (URL)</label>
<v-input v:value="form.icon" placeholder="请输入图标链接"></v-input>
</div>
<div class="form-group">
<label class="form-label">应用描述</label>
<v-input type="textarea" v:value="form.des" placeholder="请输入应用描述"></v-input>
</div>
<div class="form-actions">
<v-btn :click="saveSettings" :loading="loading" variant="primary">保存更改</v-btn>
</div>
</div>
</body>
<script setup>
form = {
name: $G.app?.name || '',
icon: $G.app?.icon || '',
des: $G.app?.des || ''
}
loading = false
saveSettings = () => {
loading = true
$axios.patch(`/api/app/${$G.app.id}`, form).then(res => {
$message.success('保存成功')
Object.assign($G.app, form)
}).catch(e => {
$message.error(e.message || '保存失败')
}).finally(() => {
loading = false
})
}
</script>
<script>
$watch(() => $G.app, () => {
if ($G.app && $G.app.id && (!$data.form.name || $data.form.name !== $G.app.name)) {
// Only update if form is empty or mismatch, but usually we just want init
// For simplicity, just update if we have data now
$data.form.name = $G.app.name
$data.form.icon = $G.app.icon
$data.form.des = $G.app.des
}
})
</script>
</html>

@ -1,137 +0,0 @@
<!doctype html>
<html>
<head>
<title>用户管理</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="用户管理" details="管理应用下的用户及其角色">
</head>
<style>
body {
padding: var(--spacing-lg);
background-color: var(--bg-color-secondary);
color: var(--text-color);
}
.header {
font-size: 1.5rem;
line-height: 1.5;
margin-bottom: var(--spacing-lg);
font-weight: 600;
color: var(--text-color);
display: flex;
align-items: center;
gap: 0.5rem;
}
.dialog {
height: 60vh;
width: 60vw;
background-color: var(--bg-color);
box-shadow: var(--shadow-lg);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
color: var(--text-color);
display: flex;
flex-direction: column;
}
.dialog-title {
font-size: 1.5rem;
margin-bottom: var(--spacing-lg);
font-weight: 600;
flex-shrink: 0;
}
/* Responsive dialog */
@media (max-width: 768px) {
.dialog {
width: 90vw;
height: 80vh;
}
}
</style>
<body>
<div class="header">
<i class="fa-solid fa-users-gear"></i> 应用用户管理
</div>
<v-table :axios='$axios' :keys="keys" api='/api/user'>
<v-btn vslot='_addon' size='sm' @click='show_user(row)' variant="primary">
<i class="fa-solid fa-id-card"></i> 权限表
</v-btn>
</v-table>
<v-dialog v:show='show'>
<div class="dialog">
<div class="dialog-title">
<i class="fa-solid fa-user-tag"></i> {{selected.username}} 角色分配
</div>
<div style="flex: 1; overflow: hidden;">
<!--
Assumption: The API to get roles for a specific user in a specific app
might need to be clarified.
If the original code had /api/user/${$G.app.id}/user_role, maybe it meant /api/app/${appId}/user_role?
Or /api/user_role?query={user_id: ...}
Looking at line 101 in original: user_role_url = `/api/user/${row.id}/user_role`
But line 51 used: :api='`/api/user/${$G.app.id}/user_role`'
I will use the one from line 101 as it seems more specific to the user,
but I need to make sure it filters by the current app if necessary.
However, based on standard REST, /api/user/{uid}/user_role likely returns roles for that user.
But we are in an "App" context.
Let's stick to a dynamic URL variable `current_user_role_url`
-->
<v-table v-if="current_user_role_url" :axios='$axios' :keys="au_keys" :api='current_user_role_url' height="100%"></v-table>
</div>
<div style="margin-top: var(--spacing-md); text-align: right;">
<v-btn @click="show = false">关闭</v-btn>
</div>
</div>
</v-dialog>
</body>
<script setup>
show = false
selected = {}
current_user_role_url = ''
auOpts = {
0: ['正常', 'positive'],
1: ['拒绝', 'warning'],
2: ['申请中', 'primary'],
3: ['禁用', 'warning'],
}
keys = [
{ name: 'id', label: 'ID', style: 'width: 4rem' },
{ name: 'username', label: '用户名', style: 'text-align: left', sortable: true },
{ name: 'nickname', label: '昵称', style: 'text-align: left', editable: true, sortable: true },
{ name: 'created_at', label: '创建时间', field: (r) => new Date(r.created_at).toLocaleString() },
{ name: 'updated_at', label: '更新时间', field: (r) => new Date(r.updated_at).toLocaleString() },
{ name: 'status', label: '账号状态', sortable: true },
]
au_keys = [
{ name: 'id', label: 'ID', no_create: true, style: 'width: 4rem' },
{ name: 'role_name', label: '角色名称' },
{ name: 'created_at', label: '创建时间', no_create: true, field: (r) => new Date(r.created_at).toLocaleString() },
{ name: 'updated_at', label: '更新时间', no_create: true, field: (r) => new Date(r.updated_at).toLocaleString() },
{ name: 'status', label: '状态', sortable: true },
]
show_user = (row) => {
selected = row
// Construct URL to fetch roles for this user.
// Assuming the intent is to see roles of this user within the context of the app or globally?
// If it's "App User Management", we probably want to see roles assigned to this user for THIS app.
// The original code had ambiguity.
// Let's assume /api/user/{uid}/user_role gets the user's roles.
// We might need to filter by app_id if the backend supports it.
// For now, I'll use the pattern from line 101 of original file.
current_user_role_url = `/api/user/${row.id}/user_role`
show = true
}
</script>
</html>

@ -1,174 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="vhtmlJS Framework" details="下一代Web组件框架">
<title>vhtmlJS - 现代Web组件框架</title>
<link rel="stylesheet" href="https://unpkg.com/animations@latest/css/animate.min.css">
</head>
<style>
body {
font-family: var(--font-family);
line-height: 1.6;
overflow-x: hidden;
margin: 0;
color: var(--text-color);
}
.navbar {
position: fixed;
width: 100%;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
z-index: 50;
box-shadow: var(--shadow-sm);
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--color-primary);
}
.nav-links {
display: flex;
gap: 1.5rem;
}
.nav-links a {
text-decoration: none;
color: var(--text-color);
transition: color 0.3s;
}
.nav-links a:hover {
color: var(--color-primary);
}
.hero {
min-height: 100vh;
display: flex;
align-items: center;
background: linear-gradient(135deg, var(--color-primary) 0%, color-mix(in srgb, var(--color-primary), #000 20%) 100%);
position: relative;
overflow: hidden;
color: white;
}
.hero-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
position: relative;
z-index: 20;
}
.hero-title {
font-size: 3.75rem;
font-weight: bold;
margin-bottom: 1.5rem;
line-height: 1.2;
}
.hero-subtitle {
font-size: 1.25rem;
opacity: 0.9;
margin-bottom: 2rem;
max-width: 48rem;
}
.hero-buttons {
display: flex;
gap: 1rem;
}
.btn-start {
background-color: white;
color: var(--color-primary);
padding: 0.75rem 1.5rem;
border-radius: 9999px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.btn-start:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.btn-github {
background-color: transparent;
border: 1px solid white;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 9999px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-github:hover {
background-color: rgba(255, 255, 255, 0.1);
}
@media (max-width: 768px) {
.hero {
padding-top: 6rem;
text-align: center;
}
.hero-title {
font-size: 2.5rem;
}
.hero-buttons {
justify-content: center;
}
}
</style>
<body>
<nav class="navbar">
<div class="nav-container">
<div class="logo">vhtmlJS</div>
<div class="nav-links">
<a href="#features">特性</a>
<a href="#docs">文档</a>
<a href="#examples">示例</a>
</div>
</div>
</nav>
<section class="hero">
<div class="hero-content">
<h1 class="hero-title animate__animated animate__fadeInUp">
下一代Web组件框架
</h1>
<p class="hero-subtitle animate__animated animate__fadeInUp animate__delay-1s">
用熟悉的HTML语法构建现代Web应用。无需构建工具开箱即用。
</p>
<div class="hero-buttons animate__animated animate__fadeInUp animate__delay-2s">
<button class="btn-start">快速开始</button>
<button class="btn-github">GitHub</button>
</div>
</div>
</section>
</body>
<script setup>
// Interactive logic can be added here
</script>
</html>

@ -1,96 +0,0 @@
<!doctype html>
<html>
<head>
<title>vbase</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="vbase Landing Page">
</head>
<style>
body {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, var(--bg-color), var(--bg-color-secondary));
color: var(--text-color);
text-align: center;
}
h1 {
font-size: 4rem;
color: var(--color-primary);
margin-bottom: var(--spacing-md);
font-weight: 800;
}
p {
font-size: 1.5rem;
color: var(--text-color-secondary);
margin-bottom: var(--spacing-xl);
max-width: 600px;
line-height: 1.6;
}
.features {
display: flex;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
}
.feature-icon {
font-size: 2rem;
color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary), transparent 90%);
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.feature-text {
font-weight: 600;
}
</style>
<body>
<h1>vbase</h1>
<p>Secure, Simple, Scalable Authentication Service for your applications.</p>
<div class="features">
<div class="feature-item">
<div class="feature-icon"><i class="fa-solid fa-shield-halved"></i></div>
<div class="feature-text">Secure</div>
</div>
<div class="feature-item">
<div class="feature-icon"><i class="fa-solid fa-bolt"></i></div>
<div class="feature-text">Fast</div>
</div>
<div class="feature-item">
<div class="feature-icon"><i class="fa-solid fa-code"></i></div>
<div class="feature-text">Developer Friendly</div>
</div>
</div>
<v-btn round size="large" :click="goLogin">Get Started <i class="fa-solid fa-arrow-right"
style="margin-left: 8px;"></i></v-btn>
</body>
<script setup>
goLogin = () => {
$router.push('/app');
}
</script>
</html>

@ -1,151 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="vhtml.js Features" details="vhtml 框架特性介绍">
<title>vhtml - 现代前端框架</title>
</head>
<style>
body {
margin: 0;
font-family: var(--font-family);
background-color: var(--bg-color-secondary);
color: var(--text-color);
}
header {
background: linear-gradient(120deg, var(--color-primary), color-mix(in srgb, var(--color-primary), #000 20%));
color: white;
padding: 40px 20px;
text-align: center;
box-shadow: var(--shadow-md);
}
header h1 {
font-size: 3rem;
margin: 0;
}
header p {
font-size: 1.2rem;
margin-top: 10px;
opacity: 0.9;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.features {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-top: 40px;
}
.feature-card {
background: var(--bg-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: 20px;
width: 300px;
text-align: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid var(--border-color);
}
.feature-card:hover {
transform: translateY(-10px);
box-shadow: var(--shadow-lg);
}
.feature-card h2 {
font-size: 1.5rem;
margin-bottom: 10px;
color: var(--color-primary);
}
.feature-card p {
font-size: 1rem;
color: var(--text-color-secondary);
}
.cta-button {
display: inline-block;
background: var(--color-primary);
color: white;
padding: 10px 20px;
border-radius: var(--radius-md);
text-decoration: none;
font-size: 1rem;
margin-top: 20px;
transition: background 0.3s ease;
border: none;
cursor: pointer;
}
.cta-button:hover {
background: color-mix(in srgb, var(--color-primary), black 10%);
}
footer {
background: var(--bg-color);
color: var(--text-color);
text-align: center;
padding: 20px;
margin-top: 40px;
border-top: 1px solid var(--border-color);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 1s ease-out;
}
</style>
<body>
<header>
<h1 class="fade-in">vhtml.js</h1>
<p class="fade-in">现代化、轻量级的前端框架</p>
<a href="#" class="cta-button fade-in">立即开始</a>
</header>
<div class="container">
<div class="features">
<div class="feature-card fade-in" v-for="feature in features">
<h2>{{ feature.title }}</h2>
<p>{{ feature.desc }}</p>
</div>
</div>
</div>
<footer>
<p>&copy; 2024 vhtml Team. All rights reserved.</p>
</footer>
</body>
<script setup>
features = [
{title: "轻量级", desc: "核心库仅 10KB加载速度飞快。"},
{title: "组件化", desc: "基于 Web Components支持原生组件复用。"},
{title: "响应式", desc: "内置响应式数据绑定,状态管理更简单。"},
{title: "零配置", desc: "无需复杂的构建工具,引入即可使用。"},
{title: "高性能", desc: "虚拟 DOM 优化,渲染性能卓越。"},
{title: "易扩展", desc: "丰富的插件系统,满足各种开发需求。"}
]
</script>
</html>

@ -1,460 +0,0 @@
<!doctype html>
<html>
<head>
<title>个人信息修改</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="个人信息修改页面" details="查看和修改用户的基本信息、联系方式等">
</head>
<style>
body {
padding: var(--spacing-lg);
background-color: var(--bg-color-secondary);
}
.profile-container {
max-width: 1000px;
margin: 0 auto;
}
.profile-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.profile-title {
font-size: 24px;
font-weight: 600;
color: var(--text-color);
}
/* 用户头像卡片 */
.avatar-section {
background: linear-gradient(135deg, var(--color-primary), color-mix(in srgb, var(--color-primary), black 20%));
color: white;
border-radius: var(--radius-lg);
padding: 32px 24px;
margin-bottom: var(--spacing-lg);
text-align: center;
box-shadow: var(--shadow-md);
}
.avatar-container {
position: relative;
display: inline-block;
margin-bottom: var(--spacing-md);
}
.avatar-preview {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 4px solid rgba(255, 255, 255, 0.3);
transition: all 0.2s ease;
}
.avatar-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
font-weight: 600;
border: 4px solid rgba(255, 255, 255, 0.3);
}
.avatar-info h3 {
font-size: 20px;
font-weight: 600;
margin: 0 0 4px 0;
}
.avatar-info p {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
/* 表单样式 */
.profile-section {
background: var(--bg-color);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
}
.section-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--border-color);
padding-bottom: var(--spacing-md);
}
.section-icon {
width: 24px;
height: 24px;
color: var(--color-primary);
font-size: 18px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-group:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--text-color);
margin-bottom: 8px;
}
.form-description {
font-size: 12px;
color: var(--text-color-secondary);
margin-top: 4px;
line-height: 1.4;
}
/* 头像输入特殊样式 */
.avatar-input-group {
display: flex;
gap: var(--spacing-md);
align-items: flex-start;
}
.avatar-input {
flex: 1;
}
.avatar-preview-small {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
object-fit: cover;
border: 2px solid var(--border-color);
flex-shrink: 0;
}
.avatar-placeholder-small {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
background: var(--bg-color-secondary);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color-secondary);
font-size: 14px;
border: 2px solid var(--border-color);
flex-shrink: 0;
}
/* 保存按钮区域 */
.save-section {
background: var(--bg-color);
border-radius: var(--radius-lg);
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
position: sticky;
bottom: var(--spacing-lg);
z-index: 100;
}
.save-info {
color: var(--text-color-secondary);
font-size: 14px;
}
.save-info.changed {
color: var(--color-primary);
font-weight: 500;
}
/* 加载状态 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
z-index: 10;
}
.loading-spinner {
color: var(--color-primary);
font-size: 24px;
}
.section-loading {
position: relative;
min-height: 200px;
}
@media (max-width: 768px) {
.profile-container {
padding: 0;
}
.save-section {
flex-direction: column;
gap: 12px;
text-align: center;
bottom: 0;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.avatar-input-group {
flex-direction: column;
}
}
</style>
<body>
<div class="profile-container">
<!-- Header -->
<div class="profile-header">
<h1 class="profile-title">个人信息</h1>
<v-btn icon variant="ghost" :click="loadUserData" title="刷新">
<i class="fa-solid fa-rotate-right"></i>
</v-btn>
</div>
<!-- Avatar Section -->
<div class="avatar-section">
<div class="avatar-container">
<img v-if="user.icon" :src="user.icon" class="avatar-preview" alt="用户头像">
<div v-else class="avatar-placeholder">
{{ user.username ? user.username.charAt(0).toUpperCase() : 'U' }}
</div>
</div>
<div class="avatar-info">
<h3>{{ user.nickname || user.username || '未设置昵称' }}</h3>
<p>用户ID: {{ user.id }}</p>
</div>
</div>
<!-- Basic Info Section -->
<div class="profile-section section-loading">
<div v-if="isLoading" class="loading-overlay">
<i class="fa-solid fa-spinner fa-spin loading-spinner"></i>
</div>
<div class="section-header">
<i class="fa-solid fa-user section-icon"></i>
<h2 class="section-title">基本信息</h2>
</div>
<div class="form-group">
<label class="form-label">用户名</label>
<v-input v:value="user.username" placeholder="请输入用户名" :disabled="isLoading"></v-input>
<div class="form-description">用户名用于登录,建议使用英文或数字</div>
</div>
<div class="form-group">
<label class="form-label">昵称</label>
<v-input v:value="user.nickname" placeholder="请输入昵称" :disabled="isLoading"></v-input>
<div class="form-description">昵称将在页面中显示,可以使用中文</div>
</div>
<div class="form-group">
<label class="form-label">头像URL</label>
<div class="avatar-input-group">
<div class="avatar-input">
<v-input v:value="user.icon" placeholder="请输入头像图片URL" :disabled="isLoading"></v-input>
<div class="form-description">输入图片链接地址支持jpg、png、gif格式</div>
</div>
<img v-if="user.icon" :src="user.icon" class="avatar-preview-small" alt="头像预览">
<div v-else class="avatar-placeholder-small"></div>
</div>
</div>
</div>
<!-- Contact Info Section -->
<div class="profile-section section-loading">
<div v-if="isLoading" class="loading-overlay">
<i class="fa-solid fa-spinner fa-spin loading-spinner"></i>
</div>
<div class="section-header">
<i class="fa-solid fa-address-book section-icon"></i>
<h2 class="section-title">联系方式</h2>
</div>
<div class="form-group">
<label class="form-label">电子邮箱</label>
<v-input v:value="user.email" placeholder="请输入电子邮箱" :disabled="isLoading"></v-input>
<div class="form-description">用于接收重要通知和找回密码</div>
</div>
<div class="form-group">
<label class="form-label">手机号码</label>
<v-input v:value="user.phone" placeholder="请输入手机号码" :disabled="isLoading"></v-input>
<div class="form-description">用于接收验证码和安全提醒</div>
</div>
</div>
<!-- Save Section -->
<div class="save-section">
<div class="save-info" :class="{ changed: hasChanges }">
{{ hasChanges ? '您有未保存的更改' : '所有信息已保存' }}
</div>
<v-btn :click="saveProfile" :disabled="!hasChanges || isSaving" :loading="isSaving">
{{ isSaving ? '保存中...' : '保存修改' }}
</v-btn>
</div>
</div>
</body>
<script setup>
// 初始化用户数据
user = {
id: $G.token?.body()?.uid,
username: '',
nickname: "",
icon: "",
email: "",
phone: "",
status: 0
}
// 原始用户数据,用于比较变更
originalUser = {}
// UI状态
isLoading = false
isSaving = false
hasChanges = false
// 检查是否有变更
checkForChanges = () => {
let changes = [
user.username !== originalUser.username,
user.nickname !== originalUser.nickname,
user.icon !== originalUser.icon,
user.email !== originalUser.email,
user.phone !== originalUser.phone
]
hasChanges = changes.some(change => change)
}
// 加载用户数据
loadUserData = async () => {
isLoading = true
const response = await $axios.get("/api/user/" + user.id).catch(error => {
console.log(error)
$message.error("加载用户数据失败")
})
if (response) {
// 确保所有字段都有值,避免 undefined
user = {
id: response.id,
username: response.username || "",
nickname: response.nickname || "",
icon: response.icon || "",
email: response.email || "",
phone: response.phone || "",
status: response.status || 0
}
// 保存原始数据
originalUser = JSON.parse(JSON.stringify(user))
hasChanges = false
}
isLoading = false
}
// 保存修改
saveProfile = async () => {
checkForChanges()
if (!hasChanges || isSaving) return
isSaving = true
// 准备更新数据
const updateData = {
username: user.username || null,
nickname: user.nickname || null,
icon: user.icon || null,
email: user.email || null,
phone: user.phone || null
}
// 发送更新请求
const response = await $axios.patch("/api/user/" + user.id, updateData).catch(error => {
$message.error("保存失败: " + error.message || "未知错误")
})
if (response) {
// 更新本地数据
user = {
...user,
username: response.username || user.username,
nickname: response.nickname || user.nickname,
icon: response.icon || user.icon,
email: response.email || user.email,
phone: response.phone || user.phone
}
// 更新原始数据
originalUser = JSON.parse(JSON.stringify(user))
hasChanges = false
$message.success("个人信息更新成功!")
}
isSaving = false
}
</script>
<script>
// 页面加载时获取用户数据
$data.loadUserData()
// 监听用户数据变化
$watch(() => {
checkForChanges()
})
// 页面离开前提醒未保存的更改
window.addEventListener('beforeunload', (event) => {
if ($data.hasChanges) {
event.preventDefault()
event.returnValue = '您有未保存的更改,确定要离开页面吗?'
return event.returnValue
}
})
</script>
</html>

@ -1,265 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="系统设置" details="管理系统配置、安全设置和个人偏好">
<title>系统设置</title>
</head>
<style>
body {
background-color: var(--bg-color-secondary);
color: var(--text-color);
padding: var(--spacing-lg);
}
.settings-container {
max-width: 800px;
margin: 0 auto;
}
.header {
margin-bottom: var(--spacing-xl);
}
.header h1 {
font-size: 24px;
color: var(--text-color);
margin: 0;
font-weight: 600;
}
.settings-section {
background: var(--bg-color);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
margin-bottom: var(--spacing-lg);
border: 1px solid var(--border-color);
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
color: var(--text-color);
}
.section-title i {
margin-right: 10px;
color: var(--color-primary);
}
.form-group {
margin-bottom: var(--spacing-md);
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-color);
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-lg);
}
.theme-options {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-sm);
}
.theme-option {
flex: 1;
padding: var(--spacing-md);
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
text-align: center;
transition: all 0.3s;
}
.theme-option.active {
border-color: var(--color-primary);
background-color: color-mix(in srgb, var(--color-primary), transparent 95%);
color: var(--color-primary);
font-weight: 600;
}
.theme-option:hover {
border-color: color-mix(in srgb, var(--color-primary), transparent 50%);
}
</style>
<body>
<div class="settings-container">
<div class="header">
<h1>系统设置</h1>
</div>
<div class="settings-section">
<div class="section-title">
<i class="fa-solid fa-user-shield"></i> 安全设置
</div>
<div class="form-group">
<label class="form-label">修改密码</label>
<div style="margin-bottom: 10px;">
<v-input type="password" placeholder="当前密码" v:value="security.currentPassword"></v-input>
</div>
<div style="margin-bottom: 10px;">
<v-input type="password" placeholder="新密码" v:value="security.newPassword"></v-input>
</div>
<div>
<v-input type="password" placeholder="确认新密码" v:value="security.confirmPassword"></v-input>
</div>
</div>
<div class="form-actions">
<v-btn :click="updatePassword" :loading="loading.password">更新密码</v-btn>
</div>
</div>
<div class="settings-section">
<div class="section-title">
<i class="fa-solid fa-palette"></i> 外观设置
</div>
<div class="form-group">
<label class="form-label">主题颜色</label>
<div class="theme-options">
<div class="theme-option" :class="{active: preferences.theme === 'light'}" @click="setTheme('light')">
<i class="fa-solid fa-sun"></i> 浅色
</div>
<div class="theme-option" :class="{active: preferences.theme === 'dark'}" @click="setTheme('dark')">
<i class="fa-solid fa-moon"></i> 深色
</div>
<div class="theme-option" :class="{active: preferences.theme === 'auto'}" @click="setTheme('auto')">
<i class="fa-solid fa-desktop"></i> 跟随系统
</div>
</div>
</div>
<div class="form-group" style="margin-top: 20px;">
<label class="form-label">紧凑模式</label>
<!-- toggle switch implementation using standard checkbox for now, could use v-switch if available -->
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" v:checked="preferences.compactMode" style="margin-right: 10px;">
<span>启用紧凑视图</span>
</label>
</div>
<div class="form-actions">
<v-btn variant="outline" :click="savePreferences" :loading="loading.preferences">保存偏好</v-btn>
</div>
</div>
<div class="settings-section">
<div class="section-title">
<i class="fa-solid fa-bell"></i> 通知设置
</div>
<div class="form-group">
<label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 10px;">
<input type="checkbox" v:checked="notifications.email" style="margin-right: 10px;">
<span>接收邮件通知</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 10px;">
<input type="checkbox" v:checked="notifications.browser" style="margin-right: 10px;">
<span>接收浏览器推送</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" v:checked="notifications.marketing" style="margin-right: 10px;">
<span>接收产品更新和营销信息</span>
</label>
</div>
<div class="form-actions">
<v-btn variant="outline" :click="saveNotifications" :loading="loading.notifications">保存设置</v-btn>
</div>
</div>
</div>
</body>
<script setup>
security = {
currentPassword: '',
newPassword: '',
confirmPassword: ''
}
preferences = {
theme: 'light',
compactMode: false
}
notifications = {
email: true,
browser: true,
marketing: false
}
loading = {
password: false,
preferences: false,
notifications: false
}
updatePassword = () => {
if (!security.currentPassword || !security.newPassword) {
$message.error('请填写完整密码信息');
return;
}
if (security.newPassword !== security.confirmPassword) {
$message.error('两次输入的新密码不一致');
return;
}
loading.password = true;
// 模拟API调用
setTimeout(() => {
loading.password = false;
$message.success('密码更新成功');
security.currentPassword = '';
security.newPassword = '';
security.confirmPassword = '';
}, 1000);
}
setTheme = (theme) => {
preferences.theme = theme;
// 这里可以添加实际切换主题的逻辑,比如 document.documentElement.setAttribute('data-theme', theme)
$message.info(`已切换到 ${theme === 'light' ? '浅色' : theme === 'dark' ? '深色' : '自动'} 主题`);
}
savePreferences = () => {
loading.preferences = true;
setTimeout(() => {
loading.preferences = false;
$message.success('偏好设置已保存');
}, 800);
}
saveNotifications = () => {
loading.notifications = true;
setTimeout(() => {
loading.notifications = false;
$message.success('通知设置已保存');
}, 800);
}
</script>
</html>

@ -1,323 +0,0 @@
<!doctype html>
<html>
<head>
<title>项目与用户管理 Dashboard</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="统计仪表盘" details="展示系统用户、项目、任务等统计数据和图表">
<script src="/assets/libs/echarts.min.js"></script>
</head>
<style>
body {
background-color: var(--bg-color-secondary);
color: var(--text-color-primary);
}
.dashboard {
max-width: 1400px;
margin: 0 auto;
padding: var(--spacing-lg);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.header h1 {
font-size: 28px;
color: var(--text-color-primary);
margin: 0;
font-weight: 600;
}
.header .date {
color: var(--text-color-secondary);
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.stat-card {
background: var(--bg-color-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
transition: transform 0.3s, box-shadow 0.3s;
border: 1px solid var(--border-color);
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
}
.stat-card .icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
font-size: 20px;
}
.stat-card .value {
font-size: 28px;
font-weight: 700;
margin-bottom: 5px;
color: var(--text-color-primary);
}
.stat-card .label {
color: var(--text-color-secondary);
font-size: 14px;
}
.stat-card .trend {
display: flex;
align-items: center;
margin-top: 10px;
font-size: 13px;
}
.trend.up {
color: var(--color-success);
}
.trend.down {
color: var(--color-danger);
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.chart-container {
background: var(--bg-color-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
}
.chart-container h2 {
margin-top: 0;
font-size: 18px;
color: var(--text-color-primary);
margin-bottom: var(--spacing-md);
}
.chart {
width: 100%;
height: 300px;
}
.recent-activities {
background: var(--bg-color-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
}
.recent-activities h2 {
margin-top: 0;
font-size: 18px;
color: var(--text-color-primary);
margin-bottom: var(--spacing-md);
}
.activity-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid var(--border-color);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-color-secondary);
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
color: var(--color-primary);
}
.activity-content {
flex: 1;
}
.activity-title {
font-weight: 600;
margin-bottom: 5px;
color: var(--text-color-primary);
}
.activity-time {
color: var(--text-color-secondary);
font-size: 13px;
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-left: 8px;
}
.badge-primary {
background: color-mix(in srgb, var(--color-primary), transparent 90%);
color: var(--color-primary);
}
.badge-success {
background: color-mix(in srgb, var(--color-success), transparent 90%);
color: var(--color-success);
}
.badge-warning {
background: color-mix(in srgb, var(--color-warning), transparent 90%);
color: var(--color-warning);
}
</style>
<body>
<div class="dashboard">
<div class="header">
<div>
<h1>仪表盘</h1>
<div class="date">{{ today }}</div>
</div>
<v-btn size="sm">
<i class="fas fa-download"></i> 导出报告
</v-btn>
</div>
<div class="stats-grid">
<div class="stat-card" v-for="(stat, index) in stats" :key="index">
<div class="icon" :style="{background: stat.bgColor, color: stat.color}">
<i :class="stat.icon"></i>
</div>
<div class="value">{{ stat.value }}</div>
<div class="label">{{ stat.label }}</div>
<div class="trend" :class="stat.trend > 0 ? 'up' : 'down'">
<i :class="stat.trend > 0 ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
<span style="margin-left: 5px;">{{ Math.abs(stat.trend) }}% 较上周</span>
</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<h2>用户增长趋势</h2>
<div id="userChart" class="chart"></div>
</div>
<div class="chart-container">
<h2>项目分布</h2>
<div id="projectChart" class="chart"></div>
</div>
</div>
<div class="recent-activities">
<h2>最近活动</h2>
<div class="activity-item" v-for="(activity, index) in activities" :key="index">
<div class="activity-icon">
<i :class="activity.icon"></i>
</div>
<div class="activity-content">
<div class="activity-title">
{{ activity.user }} {{ activity.action }}
<span class="badge" :class="activity.badgeClass">{{ activity.target }}</span>
</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
</div>
</div>
</body>
<script setup>
today = new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' });
stats = [
{ label: '总用户数', value: '12,345', icon: 'fas fa-users', color: '#4a6cf7', bgColor: 'rgba(74, 108, 247, 0.1)', trend: 5.2 },
{ label: '活跃项目', value: '86', icon: 'fas fa-project-diagram', color: '#28a745', bgColor: 'rgba(40, 167, 69, 0.1)', trend: 2.8 },
{ label: '待处理任务', value: '34', icon: 'fas fa-tasks', color: '#ffc107', bgColor: 'rgba(255, 193, 7, 0.1)', trend: -1.5 },
{ label: '系统消息', value: '128', icon: 'fas fa-envelope', color: '#dc3545', bgColor: 'rgba(220, 53, 69, 0.1)', trend: 8.4 }
];
activities = [
{ user: '张三', action: '创建了新项目', target: 'AI 助手', badgeClass: 'badge-primary', icon: 'fas fa-plus', time: '10 分钟前' },
{ user: '李四', action: '完成了任务', target: '前端重构', badgeClass: 'badge-success', icon: 'fas fa-check', time: '30 分钟前' },
{ user: '王五', action: '提交了 Bug', target: '登录问题', badgeClass: 'badge-warning', icon: 'fas fa-bug', time: '1 小时前' },
{ user: '赵六', action: '更新了文档', target: 'API 接口', badgeClass: 'badge-primary', icon: 'fas fa-file-alt', time: '2 小时前' }
];
// ECharts initialization logic will be in <script> as it needs DOM access
</script>
<script>
$watch(() => [], () => {
// Initialize charts after DOM is ready
// Mock data for charts
const userChartDom = document.getElementById('userChart');
if (userChartDom) {
const userChart = echarts.init(userChartDom);
userChart.setOption({
grid: { top: 10, right: 10, bottom: 20, left: 30 },
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
yAxis: { type: 'value' },
series: [{ data: [150, 230, 224, 218, 135, 147, 260], type: 'line', smooth: true, itemStyle: { color: '#4a6cf7' } }]
});
}
const projectChartDom = document.getElementById('projectChart');
if (projectChartDom) {
const projectChart = echarts.init(projectChartDom);
projectChart.setOption({
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
label: { show: false, position: 'center' },
emphasis: { label: { show: true, fontSize: '20', fontWeight: 'bold' } },
labelLine: { show: false },
data: [
{ value: 1048, name: 'Web' },
{ value: 735, name: 'Mobile' },
{ value: 580, name: 'Desktop' },
{ value: 484, name: 'AI' },
{ value: 300, name: 'Other' }
]
}
]
});
}
})
</script>
</html>

@ -1,197 +0,0 @@
<!doctype html>
<html>
<head>
<title>可编辑 Div 示例</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Editable Div with Suggestions">
</head>
<style>
body {
background-color: var(--bg-color-primary);
color: var(--text-color-primary);
padding: var(--spacing-lg);
}
.editable-div {
border: 1px solid var(--border-color);
padding: var(--spacing-md);
min-height: 100px;
outline: none;
white-space: pre-wrap;
background-color: var(--bg-color-secondary);
border-radius: var(--radius-md);
transition: var(--transition-base);
}
.editable-div:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary), transparent 80%);
}
.suggestions {
border: 1px solid var(--border-color);
background: var(--bg-color-primary);
max-height: 150px;
overflow-y: auto;
position: absolute;
z-index: 1000;
box-shadow: var(--shadow-lg);
border-radius: var(--radius-md);
}
.suggestion-item {
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
color: var(--text-color-primary);
}
.suggestion-item:hover {
background-color: var(--bg-color-tertiary);
}
.selected-tag {
color: var(--color-success);
font-weight: bold;
}
</style>
<style unscoped>
.test {
position: relative;
color: var(--color-danger);
display: inline-block;
min-width: 10px;
margin-left: 50px;
margin-right: 50px;
}
</style>
<body>
<div class="editable-div" contenteditable="true" @input="handleInput" @keydown="handleKeydown" ref="editableDiv">
</div>
<div class="suggestions" v-if="showSuggestions">
<div class="suggestion-item" v-for="(item, index) in filteredSuggestions" :key="index"
@mousedown="selectSuggestion(item)">
{{ item }}
</div>
</div>
</body>
<script setup>
// 初始化数据
textContent = "";
suggestions = ["Alice", "Bob", "Charlie", "David", "Eve"];
filteredSuggestions = [];
showSuggestions = false;
currentSelectionIndex = -1;
// 处理输入事件
handleInput = (e) => {
// const div = $node.querySelector(".editable-div");
// textContent = div.innerText;
// ... (logic commented out in original)
};
// 处理键盘事件(上下箭头、回车)
handleKeydown = (e) => {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
let dom = selection.anchorNode;
if (dom.nodeType === 3) {
dom = dom.parentNode;
}
if (dom && dom.hasAttribute && dom.hasAttribute('test')) {
showSuggestions = true
let sub = dom.innerText.slice(1)
console.log(sub)
filteredSuggestions = suggestions.filter((item) =>
item.toLowerCase().includes(sub)
);
} else {
showSuggestions = false
}
if (!showSuggestions) {
if (e.key === '@') {
e.preventDefault();
const div = document.createElement('div');
div.setAttribute('test', '')
div.innerText = '@'
div.classList.add('test')
// 插入换行符到当前光标位置
range.deleteContents(); // 清除选中的内容(如果有)
// range.insertNode(document.createElement('div'))
range.insertNode(div);
// 确保光标移动到换行后的位置
// const nextNode = br.nextSibling || br.parentNode.appendChild(document.createTextNode(''));
const newRange = document.createRange();
newRange.setStartAfter(div.childNodes[0]);
newRange.collapse(true);
// 更新选区
selection.removeAllRanges();
selection.addRange(newRange);
}
return
}
if (e.key === "ArrowDown") {
e.preventDefault();
currentSelectionIndex =
(currentSelectionIndex + 1) % filteredSuggestions.length;
if (isNaN(currentSelectionIndex)) {
currentSelectionIndex = 0
}
highlightSuggestion();
} else if (e.key === "ArrowUp") {
e.preventDefault();
currentSelectionIndex =
(currentSelectionIndex - 1 + filteredSuggestions.length) %
filteredSuggestions.length;
if (isNaN(currentSelectionIndex)) {
currentSelectionIndex = 0
}
highlightSuggestion();
} else if (e.key === "Enter") {
e.preventDefault();
if (currentSelectionIndex !== -1) {
let item = filteredSuggestions[currentSelectionIndex]
console.log(dom, item)
dom.innerText = '@' + item
const newRange = document.createRange();
if (!dom.nextSibling) {
dom.parentNode.appendChild(document.createTextNode(''))
}
if (dom.nextSibling.nodeType === 3) {
let span = document.createElement('span')
span.innerText = dom.nextSibling.textContent || ' '
span.style.minWidth = '10px'
dom.nextSibling.replaceWith(span)
}
let next = dom.nextSibling
if (next.nodeType === 3) {
next = next.nextSibling
}
console.log([next])
newRange.setStartAfter(next);
newRange.collapse(true);
selection.removeAllRanges(); // 清除当前的选择
selection.addRange(newRange);
dom.setAttribute('contenteditable', 'false')
dom.style.contenteditable = false
// selectSuggestion(filteredSuggestions[currentSelectionIndex]);
}
}
}
highlightSuggestion = () => {
// Logic to highlight suggestion in the list
}
selectSuggestion = (item) => {
// Logic to select suggestion
}
</script>
</html>

@ -1,114 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Custom Select Component">
</head>
<style>
.select-wrapper {
position: relative;
width: 100%;
}
.select-input {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background-color: var(--bg-color-primary);
color: var(--text-color-primary);
transition: var(--transition-base);
outline: none;
}
.select-input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary), transparent 80%);
}
.option-list {
position: absolute;
left: 0;
top: 100%;
margin-top: 4px;
background-color: var(--bg-color-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
width: 100%;
z-index: 50;
max-height: 200px;
overflow-y: auto;
}
.option-item {
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
color: var(--text-color-primary);
transition: var(--transition-base);
}
.option-item:hover {
background-color: var(--bg-color-tertiary);
}
</style>
<body>
<div class="select-wrapper">
<input type="text" class="select-input" placeholder="选择或搜索..." !value="searchText" @input="handleSearch" />
<ul class="option-list" v-if="filteredOptions.length > 0 && showOptions">
<li class="option-item" v-for="(option, index) in filteredOptions" :key="index" @click="selectOption(option)">
{{ option.label }}
</li>
</ul>
</div>
</body>
<script setup>
// 初始化数据
options = [
{ value: 'option1', label: '选项 1' },
{ value: 'option2', label: '选项 2' },
{ value: 'option3', label: '选项 3' },
{ value: 'option4', label: '选项 4' }
];
value = '';
searchText = '';
showOptions = false;
// 数据过滤结果
filteredOptions = [...options];
// 搜索处理函数
handleSearch = (e) => {
const query = e.target.value.trim().toLowerCase();
searchText = query;
filteredOptions = options.filter(option =>
option.label.toLowerCase().includes(query)
);
showOptions = true; // 只有当有输入时才显示选项列表
};
// 选项选择处理函数
selectOption = (option) => {
value = option.value;
searchText = option.label;
showOptions = false;
$data.showOptions = false; // 确保视图更新
};
</script>
<script>
// 页面加载后自动执行
window.addEventListener('click', (event) => {
if (!$node.querySelector('.select-wrapper').contains(event.target)) {
$data.showOptions = false;
}
});
</script>
</html>
Loading…
Cancel
Save