优化页面、修复BUG

This commit is contained in:
李琦
2026-06-24 17:06:22 +08:00
parent 3f653bc336
commit d5ff40e4d3
38 changed files with 1507 additions and 354 deletions

View File

@@ -5,13 +5,28 @@
<div class="toolbar">
<div class="filter-section">
<div class="filter-group">
<label for="pageSize">每页显示:</label>
<CustomSelect
v-model.number="pageSize"
:options="pageSizeOptions"
@update:modelValue="fetchLogs"
style="width: 80px;"
/>
<label>路由</label>
<input v-model="filters.path" type="text" class="filter-input" placeholder="模糊搜索路径" @keyup.enter="handleSearch" />
</div>
<div class="filter-group">
<label>归属地</label>
<input v-model="filters.region" type="text" class="filter-input" placeholder="模糊搜索归属地" @keyup.enter="handleSearch" />
</div>
<div class="filter-group">
<label>开始时间</label>
<input v-model="startDateTime" type="datetime-local" class="filter-input datetime-input" />
</div>
<div class="filter-group">
<label>结束时间</label>
<input v-model="endDateTime" type="datetime-local" class="filter-input datetime-input" />
</div>
<div class="filter-group">
<label>每页</label>
<CustomSelect v-model.number="pageSize" :options="pageSizeOptions" style="width: 80px;" />
</div>
<div class="filter-actions">
<button type="button" class="btn btn-sm btn-primary" @click="handleSearch">查询</button>
<button type="button" class="btn btn-sm btn-secondary" @click="resetFilters">重置</button>
</div>
</div>
</div>
@@ -81,12 +96,14 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, reactive } from 'vue'
import { getAccessLogs, PaginationResponse, AccessLog } from '../../services/api'
import { useToast } from '../../composables/useToast'
import CustomSelect from '../../components/CustomSelect.vue'
import { getDefaultMonthRange, toApiDateTime } from '../../utils/dateRange'
const toast = useToast()
const defaultRange = getDefaultMonthRange()
const logs = ref<PaginationResponse<AccessLog>>({
list: [],
total: 0,
@@ -95,8 +112,14 @@ const logs = ref<PaginationResponse<AccessLog>>({
})
const currentPage = ref(1)
const pageSize = ref(10)
const startDateTime = ref(defaultRange.start)
const endDateTime = ref(defaultRange.end)
const filters = reactive({
path: '',
region: ''
})
// Select options
const pageSizeOptions = [
{ value: 10, label: '10' },
{ value: 20, label: '20' },
@@ -104,19 +127,39 @@ const pageSizeOptions = [
{ value: 100, label: '100' }
]
const totalPages = computed(() => {
return Math.ceil(logs.value.total / pageSize.value)
const totalPages = computed(() => Math.ceil(logs.value.total / pageSize.value) || 1)
const buildFilters = () => ({
path: filters.path || undefined,
region: filters.region || undefined,
startDate: toApiDateTime(startDateTime.value) || undefined,
endDate: toApiDateTime(endDateTime.value) || undefined
})
const fetchLogs = async () => {
try {
logs.value = await getAccessLogs(currentPage.value, pageSize.value)
logs.value = await getAccessLogs(currentPage.value, pageSize.value, buildFilters())
} catch (error) {
console.error('Error fetching access logs:', error)
toast.error('获取访问日志失败')
}
}
const handleSearch = () => {
currentPage.value = 1
fetchLogs()
}
const resetFilters = () => {
const range = getDefaultMonthRange()
filters.path = ''
filters.region = ''
startDateTime.value = range.start
endDateTime.value = range.end
currentPage.value = 1
fetchLogs()
}
const changePage = (page: number) => {
currentPage.value = page
fetchLogs()
@@ -159,19 +202,48 @@ onMounted(() => {
.toolbar {
margin-bottom: 1rem;
display: flex;
justify-content: flex-end;
}
.filter-section {
display: flex;
gap: 1rem;
flex-wrap: wrap;
gap: 0.75rem 1rem;
align-items: flex-end;
background: rgba(255, 255, 255, 0.05);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.filter-actions {
display: flex;
gap: 0.5rem;
}
.filter-input {
padding: 0.5rem 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.375rem;
font-size: 0.875rem;
background: rgba(255, 255, 255, 0.05);
color: white;
font-family: 'Inter', sans-serif;
min-width: 140px;
}
.datetime-input {
min-width: 180px;
}
.filter-input:focus {
outline: none;
border-color: #d4b383;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
flex-direction: column;
gap: 0.25rem;
}
.filter-group label {
@@ -310,6 +382,17 @@ onMounted(() => {
font-size: 0.875rem;
}
.btn-primary {
background-color: #d4b383;
color: #050505;
border-color: #d4b383;
}
.btn-primary:hover {
background-color: transparent;
color: #d4b383;
}
.btn-secondary {
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);

View File

@@ -0,0 +1,82 @@
<template>
<div class="code-type-form-container">
<h1 class="page-title">{{ isEdit ? '编辑代码类型' : '新建代码类型' }}</h1>
<div class="form-card">
<form @submit.prevent="submitForm">
<div class="form-group">
<label class="form-label">名称</label>
<input v-model="form.name" type="text" class="form-input" placeholder="如 html、javascript" required />
</div>
<div class="form-group">
<label class="form-label">分类</label>
<CustomSelect v-model.number="form.category" :options="categoryOptions" />
</div>
<div class="form-actions">
<button type="button" class="btn-secondary" @click="router.back()">取消</button>
<button type="submit" class="btn-primary">{{ isEdit ? '保存修改' : '创建类型' }}</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import CustomSelect from '../../components/CustomSelect.vue'
import { useToast } from '../../composables/useToast'
import { getAdminCodeTypes, createCodeType, updateCodeType } from '../../services/api'
const router = useRouter()
const route = useRoute()
const toast = useToast()
const isEdit = computed(() => !!route.params.id)
const form = ref({ name: '', category: 0 })
const categoryOptions = [
{ value: 0, label: '前端' },
{ value: 1, label: '后端' },
{ value: 2, label: '其他' }
]
const loadDetail = async () => {
if (!isEdit.value) return
const id = parseInt(route.params.id as string, 10)
const list = await getAdminCodeTypes()
const item = list.find(ct => ct.id === id)
if (item) {
form.value = { name: item.name, category: item.category }
}
}
const submitForm = async () => {
try {
if (isEdit.value) {
await updateCodeType(parseInt(route.params.id as string, 10), form.value)
toast.success('更新成功')
} else {
await createCodeType(form.value)
toast.success('创建成功')
}
router.push('/admin/code-types')
} catch (error: any) {
toast.error(error.message || '提交失败')
}
}
onMounted(loadDetail)
</script>
<style scoped>
.code-type-form-container { width: 100%; max-width: 560px; }
.page-title { font-size: 1.75rem; font-weight: 600; color: #d4b383; margin-bottom: 1.5rem; }
.form-card { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; padding: 1.5rem; }
.form-group { margin-bottom: 1.25rem; }
.form-label { display: block; margin-bottom: 0.5rem; color: rgba(255,255,255,0.8); font-size: 0.875rem; }
.form-input { width: 100%; padding: 0.625rem 0.75rem; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.2); border-radius: 0.375rem; color: white; }
.form-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1.5rem; }
.btn-primary { background: #d4b383; color: #050505; border: none; padding: 0.5rem 1rem; border-radius: 0.375rem; cursor: pointer; }
.btn-secondary { background: rgba(255,255,255,0.1); color: white; border: 1px solid rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 0.375rem; cursor: pointer; }
</style>

View File

@@ -0,0 +1,89 @@
<template>
<div class="code-types-container">
<h1 class="page-title">代码类型管理</h1>
<div class="action-bar">
<button class="btn-primary" @click="router.push('/admin/code-types/create')">
<span class="btn-icon"></span>
新建类型
</button>
</div>
<div class="table-container">
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>分类</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in codeTypes" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>
<span class="category-badge">{{ categoryLabel(item.category) }}</span>
</td>
<td class="actions">
<button class="btn-edit" @click="router.push(`/admin/code-types/${item.id}/edit`)"></button>
<button class="btn-delete" @click="handleDelete(item.id)">🗑</button>
</td>
</tr>
</tbody>
</table>
<div v-if="codeTypes.length === 0" class="empty-state">暂无代码类型</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getAdminCodeTypes, deleteCodeType, CODE_TYPE_CATEGORY_LABELS, type CodeType } from '../../services/api'
import { useToast } from '../../composables/useToast'
const router = useRouter()
const toast = useToast()
const codeTypes = ref<CodeType[]>([])
const categoryLabel = (category: number) => CODE_TYPE_CATEGORY_LABELS[category] ?? '未知'
const fetchList = async () => {
try {
codeTypes.value = await getAdminCodeTypes()
} catch (error) {
console.error(error)
toast.error('加载代码类型失败')
}
}
const handleDelete = async (id: number) => {
if (!confirm('确定删除该代码类型吗?')) return
try {
await deleteCodeType(id)
toast.success('删除成功')
fetchList()
} catch (error: any) {
toast.error(error.message || '删除失败')
}
}
onMounted(fetchList)
</script>
<style scoped>
.code-types-container { width: 100%; }
.page-title { font-size: 1.75rem; font-weight: 600; color: #d4b383; margin-bottom: 1.5rem; }
.action-bar { margin-bottom: 1rem; }
.btn-primary { background: #d4b383; color: #050505; border: none; padding: 0.5rem 1rem; border-radius: 0.375rem; cursor: pointer; font-weight: 500; }
.table-container { background: rgba(255,255,255,0.05); border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.1); overflow: hidden; }
.admin-table { width: 100%; border-collapse: collapse; color: white; }
.admin-table th, .admin-table td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid rgba(255,255,255,0.1); }
.admin-table th { color: #d4b383; background: rgba(255,255,255,0.08); }
.category-badge { padding: 0.125rem 0.5rem; border-radius: 9999px; font-size: 0.75rem; background: rgba(212,179,131,0.15); color: #d4b383; }
.actions { display: flex; gap: 0.5rem; }
.btn-edit, .btn-delete { background: none; border: none; cursor: pointer; font-size: 1rem; }
.empty-state { padding: 2rem; text-align: center; color: rgba(255,255,255,0.5); }
</style>

View File

@@ -5,14 +5,41 @@
<div class="toolbar">
<div class="filter-section">
<div class="filter-group">
<label for="pageSize">每页显示:</label>
<label>操作描述</label>
<input v-model="filters.action" type="text" class="filter-input" placeholder="模糊搜索" @keyup.enter="handleSearch" />
</div>
<div class="filter-group">
<label>方法</label>
<CustomSelect v-model="filters.method" :options="methodOptions" style="width: 100px;" />
</div>
<div class="filter-group">
<label>状态</label>
<CustomSelect v-model.number="filters.status" :options="statusOptions" style="width: 100px;" />
</div>
<div class="filter-group">
<label>操作人</label>
<UserPickerPopover v-model="filters.userId" />
</div>
<div class="filter-group">
<label>开始时间</label>
<input v-model="startDateTime" type="datetime-local" class="filter-input datetime-input" />
</div>
<div class="filter-group">
<label>结束时间</label>
<input v-model="endDateTime" type="datetime-local" class="filter-input datetime-input" />
</div>
<div class="filter-group">
<label>每页</label>
<CustomSelect
v-model.number="pageSize"
:options="pageSizeOptions"
@update:modelValue="fetchLogs"
style="width: 80px;"
/>
</div>
<div class="filter-actions">
<button type="button" class="btn btn-sm btn-primary" @click="handleSearch">查询</button>
<button type="button" class="btn btn-sm btn-secondary" @click="resetFilters">重置</button>
</div>
</div>
</div>
@@ -85,12 +112,15 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, reactive } from 'vue'
import { getOperationLogs, PaginationResponse, OperationLog } from '../../services/api'
import { useToast } from '../../composables/useToast'
import CustomSelect from '../../components/CustomSelect.vue'
import UserPickerPopover from '../../components/admin/UserPickerPopover.vue'
import { getDefaultMonthRange, toApiDateTime } from '../../utils/dateRange'
const toast = useToast()
const defaultRange = getDefaultMonthRange()
const logs = ref<PaginationResponse<OperationLog>>({
list: [],
total: 0,
@@ -99,8 +129,16 @@ const logs = ref<PaginationResponse<OperationLog>>({
})
const currentPage = ref(1)
const pageSize = ref(10)
const startDateTime = ref(defaultRange.start)
const endDateTime = ref(defaultRange.end)
const filters = reactive({
action: '',
method: '',
status: 0,
userId: null as number | null
})
// Select options
const pageSizeOptions = [
{ value: 10, label: '10' },
{ value: 20, label: '20' },
@@ -108,19 +146,63 @@ const pageSizeOptions = [
{ value: 100, label: '100' }
]
const totalPages = computed(() => {
return Math.ceil(logs.value.total / pageSize.value)
const methodOptions = [
{ value: '', label: '全部' },
{ value: 'GET', label: 'GET' },
{ value: 'POST', label: 'POST' },
{ value: 'PUT', label: 'PUT' },
{ value: 'PATCH', label: 'PATCH' },
{ value: 'DELETE', label: 'DELETE' }
]
const statusOptions = [
{ value: 0, label: '全部' },
{ value: 200, label: '200' },
{ value: 201, label: '201' },
{ value: 400, label: '400' },
{ value: 401, label: '401' },
{ value: 403, label: '403' },
{ value: 404, label: '404' },
{ value: 500, label: '500' }
]
const totalPages = computed(() => Math.ceil(logs.value.total / pageSize.value) || 1)
const buildFilters = () => ({
action: filters.action || undefined,
method: filters.method || undefined,
status: filters.status || undefined,
userId: filters.userId ?? undefined,
startDate: toApiDateTime(startDateTime.value) || undefined,
endDate: toApiDateTime(endDateTime.value) || undefined
})
const fetchLogs = async () => {
try {
logs.value = await getOperationLogs(currentPage.value, pageSize.value)
logs.value = await getOperationLogs(currentPage.value, pageSize.value, buildFilters())
} catch (error) {
console.error('Error fetching operation logs:', error)
toast.error('获取操作日志失败')
}
}
const handleSearch = () => {
currentPage.value = 1
fetchLogs()
}
const resetFilters = () => {
const range = getDefaultMonthRange()
filters.action = ''
filters.method = ''
filters.status = 0
filters.userId = null
startDateTime.value = range.start
endDateTime.value = range.end
currentPage.value = 1
fetchLogs()
}
const changePage = (page: number) => {
currentPage.value = page
fetchLogs()
@@ -163,19 +245,48 @@ onMounted(() => {
.toolbar {
margin-bottom: 1rem;
display: flex;
justify-content: flex-end;
}
.filter-section {
display: flex;
gap: 1rem;
flex-wrap: wrap;
gap: 0.75rem 1rem;
align-items: flex-end;
background: rgba(255, 255, 255, 0.05);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.filter-actions {
display: flex;
gap: 0.5rem;
}
.filter-input {
padding: 0.5rem 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.375rem;
font-size: 0.875rem;
background: rgba(255, 255, 255, 0.05);
color: white;
font-family: 'Inter', sans-serif;
min-width: 140px;
}
.datetime-input {
min-width: 180px;
}
.filter-input:focus {
outline: none;
border-color: #d4b383;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
flex-direction: column;
gap: 0.25rem;
}
.filter-group label {

View File

@@ -22,15 +22,13 @@
<!-- Type Field -->
<div class="form-group">
<label for="type">类型</label>
<input
type="text"
id="type"
v-model="form.type"
placeholder="请输入代码类型js、css、html等"
required
<CustomSelect
v-model.number="form.codeTypeId"
:options="codeTypeOptions"
placeholder="请选择代码类型"
/>
<div class="error-message" v-if="errors.type">
{{ errors.type }}
<div class="error-message" v-if="errors.codeTypeId">
{{ errors.codeTypeId }}
</div>
</div>
@@ -87,7 +85,8 @@ import { MdEditor } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
import { useToast } from '../../composables/useToast'
import { createSnippet, updateSnippet, fetchSnippet } from '../../services/api'
import { createSnippet, updateSnippet, fetchSnippet, getAdminCodeTypes } from '../../services/api'
import CustomSelect from '../../components/CustomSelect.vue'
const router = useRouter()
const route = useRoute()
@@ -102,10 +101,12 @@ const errors = reactive<Record<string, string>>({})
const form = reactive({
title: '',
code: '',
type: 'js',
codeTypeId: 0,
description: ''
})
const codeTypeOptions = ref<{ value: number; label: string }[]>([])
// Validation function
const validateForm = (): boolean => {
// Reset errors
@@ -120,8 +121,8 @@ const validateForm = (): boolean => {
}
// Validate type
if (!form.type.trim()) {
errors.type = '代码类型不能为空'
if (!form.codeTypeId) {
errors.codeTypeId = '请选择代码类型'
isValid = false
}
@@ -170,14 +171,20 @@ const handleCancel = () => {
// Lifecycle
onMounted(async () => {
try {
const types = await getAdminCodeTypes()
codeTypeOptions.value = types.map(t => ({ value: t.id, label: `${t.name} (${t.category === 0 ? '前端' : t.category === 1 ? '后端' : '其他'})` }))
} catch (error) {
console.error('Failed to load code types:', error)
}
if (isEditing.value) {
try {
const snippetId = route.params.id as string
const snippet = await fetchSnippet(snippetId)
// Populate form with snippet data
form.title = snippet.title
form.code = snippet.code
form.type = snippet.type
form.codeTypeId = snippet.codeTypeId || snippet.codeType?.id || 0
form.description = snippet.description || ''
} catch (error: any) {
console.error('Failed to fetch snippet data:', error)