mirror of
https://github.com/wangdage12/Snap.Server.Web.git
synced 2026-02-17 10:02:08 +08:00
Compare commits
4 Commits
ef40a8f6f6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eca8f9856e | ||
|
|
29409e3e95 | ||
|
|
e58057665b | ||
|
|
47a4c63de2 |
20
package-lock.json
generated
20
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "ht-web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@primer/css": "^22.1.0",
|
||||
"@tsparticles/slim": "^3.9.1",
|
||||
"@tsparticles/vue3": "^3.0.1",
|
||||
"axios": "^1.13.2",
|
||||
@@ -1082,6 +1083,25 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@primer/css": {
|
||||
"version": "22.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@primer/css/-/css-22.1.0.tgz",
|
||||
"integrity": "sha512-Nwg9QaRiBeu0BU6h+Su0X07daihX1obiuqGRG8y+SexOnvWhN2J5n4OFAvGfQsit07Y7Q6gGoK+yVU5tb8CtDA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@primer/primitives": "10.x || 11.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@primer/primitives": {
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-11.4.0.tgz",
|
||||
"integrity": "sha512-JIt98Fs0c8vhOw3uNf+sxqmvCdo0VoCZPBRg4frNK/xNpDMZsQh7V0Rp7wiGbr3f1w+4oqv40sfgaftMQTnwXQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.53",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primer/css": "^22.1.0",
|
||||
"@tsparticles/slim": "^3.9.1",
|
||||
"@tsparticles/vue3": "^3.0.1",
|
||||
"axios": "^1.13.2",
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface DownloadResource {
|
||||
file_hash: string | null
|
||||
file_size: string | null
|
||||
is_active: boolean | null
|
||||
is_test?: boolean
|
||||
package_type: string
|
||||
version: string
|
||||
}
|
||||
@@ -36,6 +37,20 @@ export function getLatestVersionApi(): Promise<DownloadResource> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试版本
|
||||
* GET /download-resources?is_test=true
|
||||
*/
|
||||
export function getTestVersionApi(): Promise<DownloadResource[]> {
|
||||
return request({
|
||||
url: '/download-resources',
|
||||
method: 'get',
|
||||
params: {
|
||||
is_test: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源列表(包含未激活的)
|
||||
* GET /web-api/download-resources
|
||||
@@ -86,6 +101,7 @@ export interface CreateResourceRequest {
|
||||
file_size?: string | null
|
||||
file_hash?: string | null
|
||||
is_active?: boolean | null
|
||||
is_test?: boolean
|
||||
}
|
||||
|
||||
/** 创建资源响应数据类型 */
|
||||
|
||||
@@ -44,14 +44,22 @@ export function getUserInfoApi(): Promise<UserInfo> {
|
||||
/**
|
||||
* 获取用户列表
|
||||
* GET /web-api/users
|
||||
* @param q 搜索参数,可搜索用户名、邮箱、_id
|
||||
* @param params 搜索和筛选参数
|
||||
* @param params.q 搜索关键词,可搜索用户名、邮箱、_id
|
||||
* @param params.role 按角色筛选:maintainer, developer, user
|
||||
* @param params.email 按邮箱筛选
|
||||
* @param params.username 按用户名筛选
|
||||
* @param params.id 按用户ID筛选
|
||||
* @param params.is 按状态筛选:licensed, not-licensed
|
||||
*/
|
||||
export function getUserListApi(q?: string): Promise<UserListItem[]> {
|
||||
const params: Record<string, any> = {}
|
||||
if (q) {
|
||||
params.q = q
|
||||
}
|
||||
|
||||
export function getUserListApi(params?: {
|
||||
q?: string
|
||||
role?: string
|
||||
email?: string
|
||||
username?: string
|
||||
id?: string
|
||||
is?: string
|
||||
}): Promise<UserListItem[]> {
|
||||
return request({
|
||||
url: '/web-api/users',
|
||||
method: 'get',
|
||||
|
||||
637
src/components/GitHubSearchInput.vue
Normal file
637
src/components/GitHubSearchInput.vue
Normal file
@@ -0,0 +1,637 @@
|
||||
<template>
|
||||
<div class="github-search-container gh-search-root" :class="{ 'dark-mode': isDarkMode }">
|
||||
<div class="search-input-wrapper">
|
||||
<!-- 语法高亮显示层 -->
|
||||
<div class="syntax-highlight-layer" v-if="searchValue" v-html="highlightedText"></div>
|
||||
<!-- 实际输入框 -->
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchValue"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'has-content': searchValue }"
|
||||
:placeholder="placeholder"
|
||||
@input="handleInput"
|
||||
@keydown="handleKeydown"
|
||||
@focus="showSuggestions = true"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
<div class="search-icon">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-search">
|
||||
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自动补全建议下拉菜单 -->
|
||||
<div v-if="showSuggestions && filteredSuggestions.length > 0" class="search-suggestions">
|
||||
<div
|
||||
v-for="(suggestion, index) in filteredSuggestions"
|
||||
:key="index"
|
||||
class="suggestion-item"
|
||||
:class="{ active: selectedIndex === index }"
|
||||
@mousedown.prevent="selectSuggestion(suggestion)"
|
||||
>
|
||||
<span class="suggestion-text">{{ suggestion.text }}</span>
|
||||
<span class="suggestion-desc">{{ suggestion.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索限定符帮助提示 -->
|
||||
<div v-if="showSuggestions && currentQualifier && currentQualifierHelp" class="qualifier-help">
|
||||
<div class="help-title">{{ currentQualifierHelp.title }}</div>
|
||||
<div class="help-desc">{{ currentQualifierHelp.description }}</div>
|
||||
<div class="help-examples">
|
||||
<div v-for="(example, idx) in currentQualifierHelp.examples" :key="idx" class="example-item">
|
||||
<code>{{ example }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
interface SearchQualifier {
|
||||
key: string
|
||||
title: string
|
||||
description: string
|
||||
examples: string[]
|
||||
autocomplete?: string[]
|
||||
}
|
||||
|
||||
interface SearchSuggestion {
|
||||
text: string
|
||||
description: string
|
||||
type: 'qualifier' | 'value'
|
||||
qualifier?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'search': [query: string, filters: Record<string, string>]
|
||||
}>()
|
||||
|
||||
const searchValue = ref(props.modelValue)
|
||||
const searchInput = ref<HTMLInputElement>()
|
||||
const showSuggestions = ref(false)
|
||||
const selectedIndex = ref(0)
|
||||
const isDarkMode = ref(false)
|
||||
|
||||
// 检测暗色模式
|
||||
const checkDarkMode = () => {
|
||||
isDarkMode.value = document.documentElement.classList.contains('dark')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkDarkMode()
|
||||
// 监听主题变化
|
||||
const observer = new MutationObserver(checkDarkMode)
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
})
|
||||
onUnmounted(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
// 搜索限定符定义
|
||||
const qualifiers: SearchQualifier[] = [
|
||||
{
|
||||
key: 'role',
|
||||
title: '角色',
|
||||
description: '按用户角色筛选',
|
||||
examples: ['role:maintainer', 'role:developer', 'role:user']
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
title: '邮箱',
|
||||
description: '按邮箱地址筛选',
|
||||
examples: ['email:test@example.com', 'email:outlook.com']
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
title: '用户名',
|
||||
description: '按用户名筛选',
|
||||
examples: ['username:testuser', 'username:admin']
|
||||
},
|
||||
{
|
||||
key: 'id',
|
||||
title: '用户ID',
|
||||
description: '按用户ID筛选',
|
||||
examples: ['id:123456789', 'id:507f1f77bcf86cd799439011']
|
||||
}
|
||||
// 这个应该没必要,这个实际上就是筛选开发者权限,和role:developer重复了
|
||||
// {
|
||||
// key: 'is',
|
||||
// title: '状态',
|
||||
// description: '按权限状态筛选',
|
||||
// examples: ['is:licensed', 'is:not-licensed'],
|
||||
// autocomplete: ['licensed', 'not-licensed']
|
||||
// }
|
||||
]
|
||||
|
||||
// 检查是否为已知的限定符
|
||||
function isQualifier(text: string): boolean {
|
||||
return qualifiers.some(q => q.key === text)
|
||||
}
|
||||
|
||||
// 生成高亮的 HTML
|
||||
const highlightedText = computed(() => {
|
||||
if (!searchValue.value) return ''
|
||||
|
||||
let result = ''
|
||||
const text = searchValue.value
|
||||
|
||||
// 按空格分割成段
|
||||
const segments = text.split(' ')
|
||||
|
||||
segments.forEach((segment, index) => {
|
||||
if (segment === '') {
|
||||
// 空段表示有空格
|
||||
result += '<span class="hl-space"> </span>'
|
||||
} else {
|
||||
// 检查是否包含冒号
|
||||
const colonIndex = segment.indexOf(':')
|
||||
|
||||
if (colonIndex !== -1) {
|
||||
// 有冒号,可能是限定符:值格式
|
||||
const qualifier = segment.slice(0, colonIndex)
|
||||
const value = segment.slice(colonIndex + 1)
|
||||
|
||||
if (isQualifier(qualifier)) {
|
||||
// 已知限定符
|
||||
result += `<span class="hl-qualifier">${escapeHtml(qualifier)}</span><span class="hl-colon">:</span><span class="hl-value">${escapeHtml(value)}</span>`
|
||||
} else {
|
||||
// 未知限定符,作为普通文本处理
|
||||
result += `<span class="hl-unknown">${escapeHtml(qualifier)}</span><span class="hl-colon">:</span><span class="hl-value">${escapeHtml(value)}</span>`
|
||||
}
|
||||
} else {
|
||||
// 没有冒号,作为普通文本
|
||||
result += `<span class="hl-text">${escapeHtml(segment)}</span>`
|
||||
}
|
||||
}
|
||||
|
||||
// 添加段之间的空格(除了最后一段)
|
||||
if (index < segments.length - 1) {
|
||||
result += '<span class="hl-space"> </span>'
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// HTML 转义
|
||||
function escapeHtml(text: string): string {
|
||||
const div = document.createElement('div')
|
||||
div.textContent = text
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
// 当前输入的限定符(基于当前段)
|
||||
const currentQualifier = computed(() => {
|
||||
const lastSpaceIndex = searchValue.value.lastIndexOf(' ')
|
||||
const afterLastSpace = searchValue.value.slice(lastSpaceIndex + 1)
|
||||
const match = afterLastSpace.match(/^(\w+):/)
|
||||
return match ? match[1] : null
|
||||
})
|
||||
|
||||
// 当前限定符的帮助信息
|
||||
const currentQualifierHelp = computed(() => {
|
||||
if (!currentQualifier.value) return null
|
||||
return qualifiers.find(q => q.key === currentQualifier.value) || null
|
||||
})
|
||||
|
||||
// 建议列表
|
||||
const suggestions = computed<SearchSuggestion[]>(() => {
|
||||
const result: SearchSuggestion[] = []
|
||||
|
||||
// 检查是否正在输入新的限定符(最后一个空格后)
|
||||
const lastSpaceIndex = searchValue.value.lastIndexOf(' ')
|
||||
const afterLastSpace = searchValue.value.slice(lastSpaceIndex + 1)
|
||||
const hasColonInCurrentSegment = afterLastSpace.includes(':')
|
||||
|
||||
// 如果当前段没有冒号,则显示限定符建议
|
||||
if (!hasColonInCurrentSegment) {
|
||||
const input = afterLastSpace.toLowerCase()
|
||||
// 如果输入为空,显示所有限定符;否则显示匹配的限定符
|
||||
const shouldShowAll = input === ''
|
||||
qualifiers.forEach(q => {
|
||||
if (shouldShowAll || q.key.startsWith(input)) {
|
||||
result.push({
|
||||
text: `${q.key}:`,
|
||||
description: q.title,
|
||||
type: 'qualifier'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果当前段有冒号,则显示该限定符的值建议
|
||||
if (hasColonInCurrentSegment && currentQualifier.value && currentQualifierHelp.value) {
|
||||
const colonIndex = afterLastSpace.indexOf(':')
|
||||
const value = afterLastSpace.slice(colonIndex + 1)
|
||||
const input = value.toLowerCase()
|
||||
const qualifier = currentQualifierHelp.value
|
||||
|
||||
if (qualifier.autocomplete) {
|
||||
qualifier.autocomplete.forEach(val => {
|
||||
if (val.startsWith(input)) {
|
||||
result.push({
|
||||
text: `${qualifier.key}:${val}`,
|
||||
description: '',
|
||||
type: 'value',
|
||||
qualifier: qualifier.key
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 为特定限定符添加常用值建议
|
||||
if (qualifier.key === 'role') {
|
||||
const roles = ['maintainer', 'developer', 'user']
|
||||
roles.forEach(role => {
|
||||
if (role.startsWith(input)) {
|
||||
result.push({
|
||||
text: `role:${role}`,
|
||||
description: role === 'maintainer' ? '运维人员' : role === 'developer' ? '开发者' : '普通用户',
|
||||
type: 'value',
|
||||
qualifier: 'role'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 过滤后的建议
|
||||
const filteredSuggestions = computed(() => {
|
||||
return suggestions.value
|
||||
})
|
||||
|
||||
// 处理输入
|
||||
function handleInput() {
|
||||
emit('update:modelValue', searchValue.value)
|
||||
selectedIndex.value = 0
|
||||
// 如果有建议内容,自动显示建议框
|
||||
if (suggestions.value.length > 0) {
|
||||
showSuggestions.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 处理键盘事件
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (!showSuggestions.value || filteredSuggestions.value.length === 0) return
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
selectedIndex.value = Math.min(selectedIndex.value + 1, filteredSuggestions.value.length - 1)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
selectedIndex.value = Math.max(selectedIndex.value - 1, 0)
|
||||
break
|
||||
case 'Enter':
|
||||
const selectedSuggestion = filteredSuggestions.value[selectedIndex.value]
|
||||
if (selectedSuggestion) {
|
||||
selectSuggestion(selectedSuggestion)
|
||||
} else {
|
||||
handleSearch()
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
// ESC键关闭建议框
|
||||
showSuggestions.value = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 选择建议
|
||||
function selectSuggestion(suggestion: SearchSuggestion) {
|
||||
if (suggestion.type === 'qualifier') {
|
||||
// 如果是限定符,替换当前输入的限定符部分
|
||||
const lastSpaceIndex = searchValue.value.lastIndexOf(' ')
|
||||
const beforeLastSpace = searchValue.value.slice(0, lastSpaceIndex + 1)
|
||||
searchValue.value = beforeLastSpace + suggestion.text
|
||||
// 选择限定符后不关闭建议框,让用户继续输入值
|
||||
} else {
|
||||
// 如果是值,替换整个限定符+值,并添加空格以便输入下一个限定符
|
||||
const lastSpaceIndex = searchValue.value.lastIndexOf(' ')
|
||||
const beforeLastSpace = searchValue.value.slice(0, lastSpaceIndex + 1)
|
||||
searchValue.value = beforeLastSpace + suggestion.text + ' '
|
||||
// 选择值后重新打开建议框,方便继续输入下一个限定符
|
||||
// 使用setTimeout确保DOM更新后再打开
|
||||
setTimeout(() => {
|
||||
showSuggestions.value = true
|
||||
}, 0)
|
||||
}
|
||||
|
||||
emit('update:modelValue', searchValue.value)
|
||||
|
||||
nextTick(() => {
|
||||
searchInput.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 处理失去焦点
|
||||
function handleBlur() {
|
||||
// 延迟隐藏,以便能够点击建议项
|
||||
setTimeout(() => {
|
||||
showSuggestions.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 解析搜索查询
|
||||
function parseSearchQuery(query: string): { keyword: string; filters: Record<string, string> } {
|
||||
const filters: Record<string, string> = {}
|
||||
const parts = query.split(/\s+/)
|
||||
const keywords: string[] = []
|
||||
|
||||
parts.forEach(part => {
|
||||
if (part.includes(':')) {
|
||||
const [key, value] = part.split(':')
|
||||
if (key && value) {
|
||||
filters[key] = value
|
||||
}
|
||||
} else if (part.trim()) {
|
||||
keywords.push(part)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
keyword: keywords.join(' '),
|
||||
filters
|
||||
}
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
function handleSearch() {
|
||||
const { filters } = parseSearchQuery(searchValue.value)
|
||||
emit('search', searchValue.value, filters)
|
||||
showSuggestions.value = false
|
||||
}
|
||||
|
||||
// 监听modelValue变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
searchValue.value = newValue
|
||||
})
|
||||
|
||||
// 暴露搜索方法
|
||||
defineExpose({
|
||||
handleSearch
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 定义全局CSS变量 - 亮色模式 */
|
||||
.gh-search-root {
|
||||
--color-fg-default: #24292f;
|
||||
--color-fg-muted: #57606a;
|
||||
--color-accent-fg: #0969da;
|
||||
--color-accent-muted: rgba(9, 105, 218, 0.1);
|
||||
--color-accent-subtle: #ddf4ff;
|
||||
--color-bg-subtle: #f6f8fa;
|
||||
--color-bg-canvas: #ffffff;
|
||||
--color-canvas-overlay: #ffffff;
|
||||
--color-border-default: #d0d7de;
|
||||
--color-shadow-medium: 0 3px 6px rgba(140, 149, 159, 0.15);
|
||||
/* 高亮颜色 - 亮色模式 */
|
||||
--hl-qualifier: #cf222e;
|
||||
--hl-colon: #cf222e;
|
||||
--hl-value: #0550ae;
|
||||
--hl-text: #24292f;
|
||||
--hl-unknown: #6e7781;
|
||||
}
|
||||
|
||||
/* 定义全局CSS变量 - 暗色模式 */
|
||||
.gh-search-root.dark-mode {
|
||||
--color-fg-default: #c9d1d9;
|
||||
--color-fg-muted: #8b949e;
|
||||
--color-accent-fg: #58a6ff;
|
||||
--color-accent-muted: rgba(88, 166, 255, 0.15);
|
||||
--color-accent-subtle: rgba(88, 166, 255, 0.15);
|
||||
--color-bg-subtle: #161b22;
|
||||
--color-bg-canvas: #0d1117;
|
||||
--color-canvas-overlay: #161b22;
|
||||
--color-border-default: #30363d;
|
||||
--color-shadow-medium: 0 3px 6px rgba(0, 0, 0, 0.5);
|
||||
/* 高亮颜色 - 暗色模式 */
|
||||
--hl-qualifier: #ff7b72;
|
||||
--hl-colon: #ff7b72;
|
||||
--hl-value: #79c0ff;
|
||||
--hl-text: #c9d1d9;
|
||||
--hl-unknown: #8b949e;
|
||||
}
|
||||
|
||||
/* 语法高亮样式 - 非scoped,用于v-html生成的HTML */
|
||||
.gh-search-root .hl-qualifier {
|
||||
color: var(--hl-qualifier);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gh-search-root .hl-colon {
|
||||
color: var(--hl-colon);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gh-search-root .hl-value {
|
||||
color: var(--hl-value);
|
||||
}
|
||||
|
||||
.gh-search-root .hl-text {
|
||||
color: var(--hl-text);
|
||||
}
|
||||
|
||||
.gh-search-root .hl-unknown {
|
||||
color: var(--hl-unknown);
|
||||
}
|
||||
|
||||
.gh-search-root .hl-space {
|
||||
color: var(--color-fg-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.github-search-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: var(--color-bg-subtle);
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 语法高亮显示层 */
|
||||
.syntax-highlight-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 8px 36px 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-family: inherit;
|
||||
font-weight: normal;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
color: var(--color-fg-default);
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 8px 36px 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-family: inherit;
|
||||
font-weight: normal;
|
||||
color: var(--color-fg-default);
|
||||
caret-color: var(--color-fg-default);
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 6px;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.form-control.has-content {
|
||||
color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-control:focus + .search-icon {
|
||||
color: var(--color-accent-fg);
|
||||
}
|
||||
|
||||
.search-input-wrapper:focus-within {
|
||||
border-color: var(--color-accent-fg);
|
||||
box-shadow: 0 0 0 3px var(--color-accent-muted);
|
||||
background-color: var(--color-bg-canvas);
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: var(--color-fg-muted);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-fg-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-suggestions {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
margin-top: 4px;
|
||||
background-color: var(--color-canvas-overlay);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--color-shadow-medium);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.suggestion-item:hover,
|
||||
.suggestion-item.active {
|
||||
background-color: var(--color-accent-subtle);
|
||||
}
|
||||
|
||||
.suggestion-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent-fg);
|
||||
}
|
||||
|
||||
.suggestion-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-fg-muted);
|
||||
}
|
||||
|
||||
.qualifier-help {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
margin-top: 4px;
|
||||
padding: 12px;
|
||||
background-color: var(--color-canvas-overlay);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--color-shadow-medium);
|
||||
}
|
||||
|
||||
.help-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-fg-default);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.help-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-fg-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.help-examples {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.example-item {
|
||||
code {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
color: var(--color-accent-fg);
|
||||
background-color: var(--color-accent-subtle);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -231,6 +231,14 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="测试版">
|
||||
<el-switch
|
||||
v-model="createForm.is_test"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新功能描述">
|
||||
<el-input
|
||||
v-model="createForm.features"
|
||||
@@ -314,6 +322,14 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="测试版">
|
||||
<el-switch
|
||||
v-model="editForm.is_test"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新功能描述">
|
||||
<el-input
|
||||
v-model="editForm.features"
|
||||
@@ -380,6 +396,7 @@ const createForm = reactive<CreateResourceRequest>({
|
||||
file_size: '',
|
||||
file_hash: '',
|
||||
is_active: true,
|
||||
is_test: false,
|
||||
})
|
||||
|
||||
const createRules: FormRules = {
|
||||
@@ -410,6 +427,7 @@ const editForm = reactive<CreateResourceRequest>({
|
||||
file_size: '',
|
||||
file_hash: '',
|
||||
is_active: true,
|
||||
is_test: false,
|
||||
})
|
||||
|
||||
const editRules: FormRules = {
|
||||
@@ -499,6 +517,7 @@ function handleCreate() {
|
||||
file_size: '',
|
||||
file_hash: '',
|
||||
is_active: true,
|
||||
is_test: false,
|
||||
})
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
@@ -543,6 +562,7 @@ function handleEdit(resource: DownloadResource) {
|
||||
file_size: resource.file_size || '',
|
||||
file_hash: resource.file_hash || '',
|
||||
is_active: resource.is_active ?? true,
|
||||
is_test: resource.is_test ?? false,
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -14,11 +14,50 @@
|
||||
</h1>
|
||||
<p class="section-description">选择适合您的安装包,立即开始使用 Snap Hutao<br>系统要求:新版本Windows10及Windows11</p>
|
||||
|
||||
<!-- 版本类型切换 -->
|
||||
<div class="version-tabs">
|
||||
<el-tabs v-model="versionType" @tab-change="switchVersionType">
|
||||
<el-tab-pane label="正式版" name="stable">
|
||||
<template #label>
|
||||
<span class="tab-label">
|
||||
<el-icon><Document /></el-icon>
|
||||
正式版
|
||||
</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="测试版" name="test">
|
||||
<template #label>
|
||||
<span class="tab-label">
|
||||
<el-icon><Box /></el-icon>
|
||||
测试版
|
||||
</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 测试版警告提示 -->
|
||||
<el-alert
|
||||
v-if="versionType === 'test'"
|
||||
title="测试版说明"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<p>测试版可能存在未知的 Bug 和稳定性问题,仅供测试和体验新功能使用。如需稳定使用,请下载正式版。</p>
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 最新版本下载区域 -->
|
||||
<div v-if="latestVersion" class="latest-version-card">
|
||||
<div class="latest-header">
|
||||
<el-tag type="success" size="large" effect="dark">最新版本</el-tag>
|
||||
<el-tag
|
||||
:type="versionType === 'test' ? 'warning' : 'success'"
|
||||
size="large"
|
||||
effect="dark"
|
||||
>
|
||||
{{ versionType === 'test' ? '最新测试版本' : '最新版本' }}
|
||||
</el-tag>
|
||||
<h2 class="version-title">{{ latestVersion.version }}</h2>
|
||||
</div>
|
||||
|
||||
@@ -70,16 +109,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider>历史版本</el-divider>
|
||||
<!-- 历史版本列表(仅正式版显示) -->
|
||||
<el-divider v-if="versionType === 'stable'">历史版本</el-divider>
|
||||
|
||||
<!-- 历史版本列表 -->
|
||||
<div v-loading="loading" class="history-list">
|
||||
<div v-if="historyVersions.length === 0 && !loading" class="empty-state">
|
||||
<!-- 正式版:显示历史版本空状态 -->
|
||||
<div v-if="versionType === 'stable' && historyVersions.length === 0 && !loading" class="empty-state">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<p>暂无历史版本</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="version-table">
|
||||
<!-- 正式版:显示历史版本列表 -->
|
||||
<div v-else-if="versionType === 'stable' && historyVersions.length > 0" class="version-table">
|
||||
<div
|
||||
v-for="(item, index) in historyVersions"
|
||||
:key="index"
|
||||
@@ -129,6 +170,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试版:显示空状态 -->
|
||||
<div v-if="versionType === 'test' && !latestVersion && !loading" class="empty-state">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<p>暂无测试版本</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,7 +194,7 @@ import {
|
||||
FolderOpened,
|
||||
} from '@element-plus/icons-vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
import { getDownloadResourcesApi } from '@/api/download'
|
||||
import { getDownloadResourcesApi, getTestVersionApi } from '@/api/download'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -200,6 +247,7 @@ const latestVersion = ref<VersionInfo | null>(null)
|
||||
const historyVersions = ref<VersionInfo[]>([])
|
||||
const loading = ref(false)
|
||||
const downloading = ref(false)
|
||||
const versionType = ref<'stable' | 'test'>('stable')
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
@@ -229,7 +277,12 @@ function downloadFile(item: VersionInfo, packageType: 'msi' | 'msix') {
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = downloadUrl
|
||||
a.download = `Snap.Hutao.${item.version}.${packageType}`
|
||||
// 如果是msix的话,文件是用zip格式压缩的
|
||||
if (packageType === 'msix') {
|
||||
a.download = `Snap.Hutao.${item.version}.zip`
|
||||
} else {
|
||||
a.download = `Snap.Hutao.${item.version}.msi`
|
||||
}
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
@@ -256,7 +309,7 @@ function getTooltipText(item: VersionInfo, packageType: 'msi' | 'msix') {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载所有版本
|
||||
* 加载正式版本
|
||||
*/
|
||||
async function loadAllVersions() {
|
||||
try {
|
||||
@@ -319,6 +372,91 @@ async function loadAllVersions() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载测试版本
|
||||
*/
|
||||
async function loadTestVersion() {
|
||||
try {
|
||||
loading.value = true
|
||||
const data = await getTestVersionApi()
|
||||
if (!data || data.length === 0) {
|
||||
latestVersion.value = null
|
||||
historyVersions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 按版本号分组
|
||||
const versionMap = new Map<string, VersionInfo>()
|
||||
|
||||
data.forEach((item) => {
|
||||
if (!versionMap.has(item.version)) {
|
||||
// 首次遇到该版本,创建新记录
|
||||
versionMap.set(item.version, {
|
||||
version: item.version,
|
||||
created_at: item.created_at,
|
||||
created_by: item.created_by,
|
||||
features: item.features,
|
||||
file_hash: item.file_hash,
|
||||
file_size: item.file_size,
|
||||
is_active: item.is_active,
|
||||
packages: {
|
||||
msi: null,
|
||||
msix: null,
|
||||
msi_size: null,
|
||||
msix_size: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 添加包类型的下载链接和大小
|
||||
const versionInfo = versionMap.get(item.version)
|
||||
if (versionInfo) {
|
||||
if (item.package_type === 'msi') {
|
||||
versionInfo.packages.msi = item.download_url
|
||||
versionInfo.packages.msi_size = item.file_size
|
||||
} else if (item.package_type === 'msix') {
|
||||
versionInfo.packages.msix = item.download_url
|
||||
versionInfo.packages.msix_size = item.file_size
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 转换为数组并按创建时间倒序排序
|
||||
const versions = Array.from(versionMap.values()).sort((a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
)
|
||||
|
||||
if (versions.length > 0) {
|
||||
latestVersion.value = versions[0] ?? null
|
||||
// 测试版本只显示最新版本,不显示历史版本
|
||||
historyVersions.value = []
|
||||
} else {
|
||||
latestVersion.value = null
|
||||
historyVersions.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载测试版本失败:', error)
|
||||
ElMessage.error('加载测试版本失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换版本类型
|
||||
*/
|
||||
function switchVersionType(type: 'stable' | 'test') {
|
||||
versionType.value = type
|
||||
latestVersion.value = null
|
||||
historyVersions.value = []
|
||||
|
||||
if (type === 'stable') {
|
||||
loadAllVersions()
|
||||
} else {
|
||||
loadTestVersion()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAllVersions()
|
||||
})
|
||||
@@ -360,7 +498,42 @@ onMounted(() => {
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 版本类型切换 */
|
||||
.version-tabs {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.version-tabs :deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.version-tabs :deep(.el-tabs__nav-wrap::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.version-tabs :deep(.el-tabs__item) {
|
||||
font-size: 16px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.version-tabs .tab-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.version-tabs :deep(.el-alert) {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.version-tabs :deep(.el-alert__description) {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 最新版本卡片 */
|
||||
|
||||
@@ -2,28 +2,27 @@
|
||||
<div class="user-management">
|
||||
<!-- 搜索栏和用户统计并排 -->
|
||||
<div class="search-statistics-row">
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入用户名、邮箱或ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
<!-- GitHub样式搜索框 -->
|
||||
<div class="search-container">
|
||||
<GitHubSearchInput
|
||||
ref="githubSearch"
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索用户... 例如: role:developer username:test"
|
||||
@search="handleGitHubSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 用户统计 -->
|
||||
<div class="statistics" v-if="!loading && displayUserList.length > 0">
|
||||
<el-statistic title="当前显示用户数" :value="displayUserList.length" />
|
||||
<el-statistic title="运维人员" :value="maintainerCount" />
|
||||
<el-statistic title="开发者" :value="developerCount" />
|
||||
<el-statistic v-if="isSearchMode" title="搜索模式" value="进行中" />
|
||||
</div>
|
||||
<div class="search-actions">
|
||||
<el-button type="primary" @click="executeSearch" :loading="loading">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户统计 -->
|
||||
<div class="statistics" v-if="!loading && displayUserList.length > 0">
|
||||
<el-statistic title="当前显示用户数" :value="displayUserList.length" />
|
||||
<el-statistic title="运维人员" :value="maintainerCount" />
|
||||
<el-statistic title="开发者" :value="developerCount" />
|
||||
<el-statistic v-if="isSearchMode" title="搜索模式" value="进行中" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
@@ -67,45 +66,41 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import GitHubSearchInput from '@/components/GitHubSearchInput.vue'
|
||||
import { getUserListApi, type UserListItem } from '@/api/user'
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
}
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const githubSearch = ref<InstanceType<typeof GitHubSearchInput>>()
|
||||
const searchQuery = ref('')
|
||||
const currentFilters = ref<Record<string, string>>({})
|
||||
const userList = ref<UserListItem[]>([])
|
||||
const loading = ref(false)
|
||||
const isSearchMode = ref(false)
|
||||
|
||||
// 显示的用户列表(根据是否在搜索模式决定显示全部还是搜索结果)
|
||||
// 显示的用户列表(直接使用后端返回的数据)
|
||||
const displayUserList = computed(() => {
|
||||
return userList.value
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
// 统计数据(基于当前显示的列表)
|
||||
const maintainerCount = computed(() =>
|
||||
userList.value.filter(user => user.IsMaintainer).length
|
||||
displayUserList.value.filter(user => user.IsMaintainer).length
|
||||
)
|
||||
|
||||
const developerCount = computed(() =>
|
||||
userList.value.filter(user => user.IsLicensedDeveloper).length
|
||||
displayUserList.value.filter(user => user.IsLicensedDeveloper).length
|
||||
)
|
||||
|
||||
// 获取用户列表
|
||||
async function fetchUserList(searchKeyword?: string) {
|
||||
async function fetchUserList(filters?: Record<string, string>) {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getUserListApi(searchKeyword)
|
||||
const data = await getUserListApi(filters)
|
||||
userList.value = data
|
||||
|
||||
if (searchKeyword) {
|
||||
ElMessage.success(`搜索完成,找到 ${data.length} 个匹配的用户`)
|
||||
if (filters && Object.keys(filters).length > 0) {
|
||||
ElMessage.success(`找到 ${data.length} 个匹配的用户`)
|
||||
isSearchMode.value = true
|
||||
} else {
|
||||
ElMessage.success('用户列表加载成功')
|
||||
@@ -120,40 +115,37 @@ async function fetchUserList(searchKeyword?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!searchForm.keyword.trim()) {
|
||||
ElMessage.warning('请输入搜索关键词')
|
||||
return
|
||||
// 处理GitHub搜索
|
||||
function handleGitHubSearch(_query: string, filters: Record<string, string>) {
|
||||
currentFilters.value = filters
|
||||
fetchUserList(filters)
|
||||
}
|
||||
|
||||
// 执行搜索(通过搜索框)
|
||||
function executeSearch() {
|
||||
if (githubSearch.value) {
|
||||
githubSearch.value.handleSearch()
|
||||
} else {
|
||||
handleGitHubSearch(searchQuery.value, currentFilters.value)
|
||||
}
|
||||
fetchUserList(searchForm.keyword.trim())
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
isSearchMode.value = false
|
||||
fetchUserList() // 重新获取全部用户列表
|
||||
searchQuery.value = ''
|
||||
currentFilters.value = {}
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
function handleRefresh() {
|
||||
if (isSearchMode.value && searchForm.keyword) {
|
||||
fetchUserList(searchForm.keyword)
|
||||
if (isSearchMode.value && Object.keys(currentFilters.value).length > 0) {
|
||||
fetchUserList(currentFilters.value)
|
||||
} else {
|
||||
fetchUserList()
|
||||
}
|
||||
}
|
||||
|
||||
// function handleAdd() {
|
||||
// ElMessage.info('新增用户功能待实现')
|
||||
// }
|
||||
|
||||
// function handleEdit(row: UserListItem) {
|
||||
// ElMessage.info(`编辑用户 ${row.UserName} 功能待实现`)
|
||||
// }
|
||||
|
||||
// function handleDelete(row: UserListItem) {
|
||||
// ElMessage.info(`删除用户 ${row.UserName} 功能待实现`)
|
||||
// }
|
||||
|
||||
// 页面加载时获取用户列表
|
||||
onMounted(() => {
|
||||
fetchUserList()
|
||||
@@ -164,25 +156,58 @@ onMounted(() => {
|
||||
.user-management {
|
||||
padding: 24px;
|
||||
}
|
||||
/* 新增flex布局 */
|
||||
|
||||
.search-statistics-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
gap: 24px;
|
||||
}
|
||||
.search-form {
|
||||
/* 保持原有样式,可根据需要调整宽度 */
|
||||
margin-bottom: 0;
|
||||
|
||||
.search-container {
|
||||
width: 600px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-container :deep(.github-search-container) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.statistics {
|
||||
margin-left: 32px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.search-statistics-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.statistics {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user