mirror of https://github.com/veypi/OneAuth.git
remove old
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
|
||||
```
|
||||
@ -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]) || ' '}}
|
||||
</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,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,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,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,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…
Reference in New Issue