优化页面、修复BUG
This commit is contained in:
@@ -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);
|
||||
|
||||
82
client/src/pages/admin/CodeTypeForm.vue
Normal file
82
client/src/pages/admin/CodeTypeForm.vue
Normal 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>
|
||||
89
client/src/pages/admin/CodeTypes.vue
Normal file
89
client/src/pages/admin/CodeTypes.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user