优化页面、修复BUG
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
<div class="inquiries-container">
|
||||
<h1 class="page-title">合作咨询管理</h1>
|
||||
|
||||
<!-- Inquiries Table -->
|
||||
<div class="table-container">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
@@ -41,15 +40,22 @@
|
||||
{{ getStatusLabel(item.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="table-cell">{{ item.createdAt ? new Date(item.createdAt).toLocaleString() : '-' }}</td>
|
||||
<td class="table-cell">{{ formatCreatedAt(item.createdAt) }}</td>
|
||||
<td class="table-cell actions">
|
||||
<button
|
||||
class="btn-action btn-view"
|
||||
@click="openDetail(item)"
|
||||
title="查看详情"
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
<button
|
||||
v-if="item.status === 0"
|
||||
class="btn-action btn-read"
|
||||
@click="updateStatus(item.id!, 1)"
|
||||
title="标记为已读"
|
||||
>
|
||||
👁️
|
||||
已读
|
||||
</button>
|
||||
<button
|
||||
v-if="item.status !== 2"
|
||||
@@ -57,18 +63,23 @@
|
||||
@click="updateStatus(item.id!, 2)"
|
||||
title="标记为已联系"
|
||||
>
|
||||
✅
|
||||
已联系
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="inquiries.length === 0" class="empty-state">
|
||||
<p>暂无咨询数据</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InquiryDetailModal
|
||||
v-model:is-open="isDetailModalOpen"
|
||||
:inquiry="selectedInquiry"
|
||||
@update-status="handleDetailStatusUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -76,9 +87,12 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { API_BASE, authFetchJson, fetchInquiries, getAuthHeaders, type Inquiry } from '../../services/api'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
import InquiryDetailModal from '../../components/admin/InquiryDetailModal.vue'
|
||||
|
||||
const toast = useToast()
|
||||
const inquiries = ref<Inquiry[]>([])
|
||||
const isDetailModalOpen = ref(false)
|
||||
const selectedInquiry = ref<Inquiry | null>(null)
|
||||
|
||||
const fetchInquiriesList = async () => {
|
||||
try {
|
||||
@@ -89,6 +103,11 @@ const fetchInquiriesList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const openDetail = (item: Inquiry) => {
|
||||
selectedInquiry.value = item
|
||||
isDetailModalOpen.value = true
|
||||
}
|
||||
|
||||
const updateStatus = async (id: number, status: number) => {
|
||||
try {
|
||||
await authFetchJson(`${API_BASE}/admin/inquiries/${id}/status`, {
|
||||
@@ -97,13 +116,32 @@ const updateStatus = async (id: number, status: number) => {
|
||||
body: JSON.stringify({ status })
|
||||
})
|
||||
toast.success('状态更新成功')
|
||||
fetchInquiriesList()
|
||||
await fetchInquiriesList()
|
||||
if (selectedInquiry.value?.id === id) {
|
||||
selectedInquiry.value = inquiries.value.find(item => item.id === id) || selectedInquiry.value
|
||||
if (selectedInquiry.value) {
|
||||
selectedInquiry.value.status = status
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
toast.error(error.message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDetailStatusUpdate = async (status: number) => {
|
||||
if (!selectedInquiry.value?.id) return
|
||||
await updateStatus(selectedInquiry.value.id, status)
|
||||
}
|
||||
|
||||
const formatCreatedAt = (createdAt?: string | number) => {
|
||||
if (createdAt === undefined || createdAt === null || createdAt === '') return '-'
|
||||
const timestamp = typeof createdAt === 'number' ? createdAt : Number(createdAt)
|
||||
if (Number.isNaN(timestamp)) return '-'
|
||||
const ms = timestamp > 1e12 ? timestamp : timestamp * 1000
|
||||
return new Date(ms).toLocaleString()
|
||||
}
|
||||
|
||||
const getContactMethodLabel = (method: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'wechat': '微信',
|
||||
@@ -198,17 +236,26 @@ onMounted(() => {
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 0.5rem;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background: rgba(212, 179, 131, 0.12);
|
||||
color: rgba(212, 179, 131, 0.95);
|
||||
}
|
||||
.btn-view:hover {
|
||||
background: rgba(212, 179, 131, 0.22);
|
||||
}
|
||||
|
||||
.btn-read {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: rgba(59, 130, 246, 0.8);
|
||||
@@ -230,4 +277,4 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -60,25 +60,54 @@
|
||||
<tr v-for="post in posts" :key="post.id" class="group transition-colors duration-200">
|
||||
<td class="font-mono text-xs text-white/40">#{{ post.id }}</td>
|
||||
<td class="font-medium text-white group-hover:text-art-accent transition-colors">{{ post.title }}</td>
|
||||
<td><span class="px-2 py-1 rounded bg-white/5 border border-white/5 text-xs text-art-muted">{{ post.categoryName || post.category?.name || '未分类' }}</span></td>
|
||||
<td>
|
||||
<span v-if="post.columnName" class="px-2 py-1 rounded bg-art-accent/10 border border-art-accent/20 text-xs text-art-accent">
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 rounded bg-white/5 border border-white/5 text-xs text-art-muted cursor-pointer hover:border-art-accent/40 hover:text-white transition-colors"
|
||||
@click="openRelationModal(post, 'category')"
|
||||
>
|
||||
{{ post.categoryName || post.category?.name || '未分类' }}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
v-if="post.columnName"
|
||||
type="button"
|
||||
class="px-2 py-1 rounded bg-art-accent/10 border border-art-accent/20 text-xs text-art-accent cursor-pointer hover:border-art-accent/40 transition-colors"
|
||||
@click="openRelationModal(post, 'column')"
|
||||
>
|
||||
{{ post.columnName }}
|
||||
</span>
|
||||
<span v-else class="text-art-muted text-xs">-</span>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="px-2 py-0.5 rounded border border-dashed border-white/20 text-xs text-art-muted/60 cursor-pointer hover:border-art-accent/40 hover:text-art-muted transition-colors"
|
||||
@click="openRelationModal(post, 'column')"
|
||||
>
|
||||
无专栏
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="post.tags && post.tags.length > 0" class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="tag in post.tags.slice(0, 3)"
|
||||
:key="tag.id"
|
||||
class="px-2 py-0.5 rounded bg-white/5 border border-white/5 text-xs text-art-muted"
|
||||
<button
|
||||
v-for="tag in post.tags.slice(0, 3)"
|
||||
:key="tag.id"
|
||||
type="button"
|
||||
class="px-2 py-0.5 rounded bg-white/5 border border-white/5 text-xs text-art-muted cursor-pointer hover:border-art-accent/40 hover:text-white transition-colors"
|
||||
@click="openRelationModal(post, 'tags')"
|
||||
>
|
||||
#{{ tag.name }}
|
||||
</span>
|
||||
<span v-if="post.tags.length > 3" class="text-art-muted text-xs">+{{ post.tags.length - 3 }}</span>
|
||||
</button>
|
||||
<span v-if="post.tags.length > 3" class="text-art-muted text-xs self-center">+{{ post.tags.length - 3 }}</span>
|
||||
</div>
|
||||
<span v-else class="text-art-muted text-xs">-</span>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="px-2 py-0.5 rounded border border-dashed border-white/20 text-xs text-art-muted/60 cursor-pointer hover:border-art-accent/40 hover:text-art-muted transition-colors"
|
||||
@click="openRelationModal(post, 'tags')"
|
||||
>
|
||||
无标签
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-art-muted text-xs">{{ post.date }}</td>
|
||||
<td>
|
||||
@@ -93,17 +122,20 @@
|
||||
<td>
|
||||
<button
|
||||
@click="openAccessLogModal(post)"
|
||||
class="admin-btn-secondary py-1 px-3 text-xs opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
||||
class="admin-btn-secondary py-1 px-3 text-xs"
|
||||
title="查看访问记录"
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<div class="flex items-center justify-end gap-2 flex-wrap">
|
||||
<button @click="openRelationModal(post)" class="admin-btn-secondary py-1 px-3 text-xs">
|
||||
管理
|
||||
</button>
|
||||
<button @click="openHistoryModal(post)" class="admin-btn-secondary py-1 px-3 text-xs">
|
||||
历史
|
||||
</button>
|
||||
<router-link :to="`/admin/posts/${post.id}/edit`" class="admin-btn-secondary py-1 px-3 text-xs">
|
||||
编辑
|
||||
</router-link>
|
||||
@@ -169,9 +201,17 @@
|
||||
<PostRelationModal
|
||||
v-model:isOpen="isRelationModalOpen"
|
||||
:post="editingPost"
|
||||
:focus-section="relationFocusSection"
|
||||
@saved="fetchPosts"
|
||||
/>
|
||||
|
||||
<PostHistoryModal
|
||||
:is-open="isHistoryModalOpen"
|
||||
:post-id="historyPostId"
|
||||
@close="isHistoryModalOpen = false"
|
||||
@restored="fetchPosts"
|
||||
/>
|
||||
|
||||
<!-- Access Log Modal -->
|
||||
<AccessLogModal
|
||||
v-model:isOpen="isAccessLogModalOpen"
|
||||
@@ -186,12 +226,18 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { getAdminPosts, deletePost as deletePostApi, Post, togglePostStatus } from '../../services/api'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
import PostRelationModal from '../../components/admin/PostRelationModal.vue'
|
||||
import PostHistoryModal from '../../components/admin/PostHistoryModal.vue'
|
||||
import AccessLogModal from '../../components/admin/AccessLogModal.vue'
|
||||
|
||||
type RelationFocusSection = 'category' | 'column' | 'tags' | 'all'
|
||||
|
||||
const toast = useToast()
|
||||
const posts = ref<Post[]>([])
|
||||
const isRelationModalOpen = ref(false)
|
||||
const editingPost = ref<Post | null>(null)
|
||||
const relationFocusSection = ref<RelationFocusSection>('all')
|
||||
const isHistoryModalOpen = ref(false)
|
||||
const historyPostId = ref<number | string>('')
|
||||
const isAccessLogModalOpen = ref(false)
|
||||
const selectedPostId = ref<number | null>(null)
|
||||
const selectedPostTitle = ref<string>('')
|
||||
@@ -226,11 +272,17 @@ const visiblePages = computed(() => {
|
||||
return pages
|
||||
})
|
||||
|
||||
const openRelationModal = (post: Post) => {
|
||||
const openRelationModal = (post: Post, section: RelationFocusSection = 'all') => {
|
||||
editingPost.value = post
|
||||
relationFocusSection.value = section
|
||||
isRelationModalOpen.value = true
|
||||
}
|
||||
|
||||
const openHistoryModal = (post: Post) => {
|
||||
historyPostId.value = post.id
|
||||
isHistoryModalOpen.value = true
|
||||
}
|
||||
|
||||
const openAccessLogModal = (post: Post) => {
|
||||
selectedPostId.value = post.id
|
||||
selectedPostTitle.value = post.title
|
||||
|
||||
@@ -1,160 +1,222 @@
|
||||
<template>
|
||||
<div class="snippet-form-container">
|
||||
<h1 class="page-title">{{ isEditing ? '编辑代码片段' : '新建代码片段' }}</h1>
|
||||
<div class="w-full h-[calc(100vh-8rem)] flex flex-col animate-reveal">
|
||||
<!-- Top Bar -->
|
||||
<div class="flex items-center justify-between mb-4 shrink-0 gap-3">
|
||||
<h1 class="text-xl md:text-2xl font-serif italic text-white truncate">
|
||||
{{ isEditing ? '编辑代码片段' : '新建代码片段' }}
|
||||
</h1>
|
||||
<div class="flex gap-2 md:gap-3 shrink-0">
|
||||
<button type="button" class="admin-btn-secondary text-sm" @click="handleCancel">
|
||||
取消
|
||||
</button>
|
||||
<button type="button" class="admin-btn-primary text-sm" @click="handleSubmit" :disabled="isSubmitting">
|
||||
{{ isSubmitting ? '提交中...' : (isEditing ? '更新' : '创建') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<form @submit.prevent="handleSubmit" class="snippet-form">
|
||||
<!-- Title Field -->
|
||||
<div class="form-group">
|
||||
<label for="title">标题</label>
|
||||
<input
|
||||
<form @submit.prevent="handleSubmit" class="flex-1 flex flex-col lg:flex-row gap-4 lg:gap-6 overflow-hidden min-h-0">
|
||||
<!-- Left: Code / Description tabs -->
|
||||
<div class="flex-1 flex flex-col min-w-0 min-h-[45vh] lg:min-h-0 h-full">
|
||||
<div class="admin-card flex-1 flex flex-col overflow-hidden border border-white/10 min-h-0">
|
||||
<div class="flex border-b border-white/10 shrink-0">
|
||||
<button
|
||||
v-for="tab in editorTabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2.5 text-xs font-mono transition-colors"
|
||||
:class="editorTab === tab.id
|
||||
? 'text-art-accent bg-white/5 border-b-2 border-art-accent'
|
||||
: 'text-art-muted hover:text-white'"
|
||||
@click="editorTab = tab.id"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-show="editorTab === 'code'" class="flex-1 flex flex-col min-h-0 focus-within:border-art-accent/50">
|
||||
<MdEditor
|
||||
v-model="form.code"
|
||||
theme="dark"
|
||||
:preview="false"
|
||||
placeholder="请输入代码..."
|
||||
class="custom-md-editor flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="editorTab === 'description'"
|
||||
class="flex-1 flex flex-col min-h-0"
|
||||
@paste="handleVideoPaste"
|
||||
>
|
||||
<MdEditor
|
||||
ref="descEditorRef"
|
||||
v-model="form.description"
|
||||
theme="dark"
|
||||
:preview="true"
|
||||
placeholder="支持 Markdown,可上传图片、粘贴或拖拽视频..."
|
||||
class="custom-md-editor desc-md-editor flex-1"
|
||||
@onUploadImg="handleUploadImg"
|
||||
@onDrop="handleEditorDrop"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-message mt-1" v-if="errors.code && editorTab === 'code'">{{ errors.code }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Meta & Settings -->
|
||||
<div class="w-full lg:w-80 shrink-0 overflow-y-auto custom-scrollbar min-h-0 lg:max-h-full">
|
||||
<div class="admin-card p-5 md:p-6 space-y-5">
|
||||
<h3 class="text-sm font-medium text-white border-b border-white/5 pb-3 flex items-center gap-2">
|
||||
<span class="text-art-accent">⚙</span> 片段配置
|
||||
</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="title" class="block text-xs font-medium text-art-muted uppercase tracking-wider">标题</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
v-model="form.title"
|
||||
class="admin-input"
|
||||
placeholder="请输入代码片段标题"
|
||||
required
|
||||
/>
|
||||
<div class="error-message" v-if="errors.title">
|
||||
{{ errors.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type Field -->
|
||||
<div class="form-group">
|
||||
<label for="type">类型</label>
|
||||
<CustomSelect
|
||||
v-model.number="form.codeTypeId"
|
||||
:options="codeTypeOptions"
|
||||
placeholder="请选择代码类型"
|
||||
/>
|
||||
<div class="error-message" v-if="errors.codeTypeId">
|
||||
{{ errors.codeTypeId }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Code Field - Updated to MdEditor for better editing experience -->
|
||||
<div class="form-group">
|
||||
<label>代码内容</label>
|
||||
<div class="editor-wrapper">
|
||||
<MdEditor
|
||||
v-model="form.code"
|
||||
theme="dark"
|
||||
:preview="false"
|
||||
placeholder="请输入代码..."
|
||||
class="custom-md-editor"
|
||||
/>
|
||||
<div class="error-message" v-if="errors.title">{{ errors.title }}</div>
|
||||
</div>
|
||||
<div class="error-message" v-if="errors.code">
|
||||
{{ errors.code }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Field -->
|
||||
<div class="form-group">
|
||||
<label for="description">描述</label>
|
||||
<textarea
|
||||
id="description"
|
||||
v-model="form.description"
|
||||
placeholder="请输入代码片段描述"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="error-message" v-if="errors.description">
|
||||
{{ errors.description }}
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">类型</label>
|
||||
<CustomSelect
|
||||
v-model.number="form.codeTypeId"
|
||||
:options="codeTypeOptions"
|
||||
placeholder="请选择代码类型"
|
||||
/>
|
||||
<div class="error-message" v-if="errors.codeTypeId">{{ errors.codeTypeId }}</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">
|
||||
关联文章 <span class="normal-case text-art-muted/70">(可多选)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="postSearch"
|
||||
type="text"
|
||||
placeholder="搜索文章标题..."
|
||||
class="admin-input text-sm"
|
||||
/>
|
||||
<div class="post-chip-list">
|
||||
<button
|
||||
v-for="p in filteredPosts"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="post-chip"
|
||||
:class="{ 'post-chip--active': form.postIds.includes(p.id) }"
|
||||
@click="togglePost(p.id)"
|
||||
>
|
||||
{{ p.title }}
|
||||
</button>
|
||||
<div v-if="filteredPosts.length === 0" class="text-xs text-art-muted italic py-2">
|
||||
暂无匹配文章
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="form-actions">
|
||||
<button type="button" class="cancel-btn" @click="handleCancel">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" class="submit-btn" :disabled="isSubmitting">
|
||||
{{ isSubmitting ? '提交中...' : (isEditing ? '更新代码片段' : '创建代码片段') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
// Import MdEditor
|
||||
import { MdEditor } from 'md-editor-v3'
|
||||
import type { ExposeParam } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
|
||||
import { useToast } from '../../composables/useToast'
|
||||
import { createSnippet, updateSnippet, fetchSnippet, getAdminCodeTypes } from '../../services/api'
|
||||
import { useMdMediaUpload } from '../../composables/useMdMediaUpload'
|
||||
import { createSnippet, updateSnippet, fetchSnippet, getAdminCodeTypes, getAdminPosts, Post } from '../../services/api'
|
||||
import CustomSelect from '../../components/CustomSelect.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
|
||||
// Form state
|
||||
const descEditorRef = ref<ExposeParam>()
|
||||
const { handleUploadImg, handleEditorDrop, handleVideoPaste } = useMdMediaUpload(descEditorRef)
|
||||
|
||||
const editorTab = ref<'code' | 'description'>('code')
|
||||
const editorTabs = [
|
||||
{ id: 'code' as const, label: '代码' },
|
||||
{ id: 'description' as const, label: '备注' },
|
||||
]
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
const isEditing = computed(() => !!route.params.id)
|
||||
const errors = reactive<Record<string, string>>({})
|
||||
|
||||
// Form data
|
||||
const form = reactive({
|
||||
title: '',
|
||||
code: '',
|
||||
codeTypeId: 0,
|
||||
description: ''
|
||||
description: '',
|
||||
postIds: [] as number[]
|
||||
})
|
||||
|
||||
const codeTypeOptions = ref<{ value: number; label: string }[]>([])
|
||||
const adminPosts = ref<Post[]>([])
|
||||
const postSearch = ref('')
|
||||
|
||||
const filteredPosts = computed(() => {
|
||||
const q = postSearch.value.trim().toLowerCase()
|
||||
if (!q) return adminPosts.value
|
||||
return adminPosts.value.filter(p => p.title.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
const togglePost = (postId: number) => {
|
||||
const idx = form.postIds.indexOf(postId)
|
||||
if (idx === -1) {
|
||||
form.postIds.push(postId)
|
||||
} else {
|
||||
form.postIds.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Validation function
|
||||
const validateForm = (): boolean => {
|
||||
// Reset errors
|
||||
Object.keys(errors).forEach(key => delete errors[key])
|
||||
|
||||
let isValid = true
|
||||
|
||||
// Validate title
|
||||
if (!form.title.trim()) {
|
||||
errors.title = '代码片段标题不能为空'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (!form.codeTypeId) {
|
||||
errors.codeTypeId = '请选择代码类型'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Validate code
|
||||
if (!form.code.trim()) {
|
||||
errors.code = '代码内容不能为空'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
// Submit handler
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
if (!validateForm()) return
|
||||
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
// Update existing snippet
|
||||
await updateSnippet(route.params.id as string, form)
|
||||
toast.success('代码片段更新成功')
|
||||
} else {
|
||||
// Create new snippet
|
||||
await createSnippet(form)
|
||||
const result = await createSnippet(form)
|
||||
toast.success('代码片段创建成功')
|
||||
if (result.id) {
|
||||
router.push(`/admin/snippets/${result.id}/edit`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to snippets list
|
||||
router.push('/admin/snippets')
|
||||
} catch (error: any) {
|
||||
console.error('Error submitting form:', error)
|
||||
@@ -164,18 +226,20 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel handler
|
||||
const handleCancel = () => {
|
||||
router.push('/admin/snippets')
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const types = await getAdminCodeTypes()
|
||||
const [types, postsRes] = await Promise.all([
|
||||
getAdminCodeTypes(),
|
||||
getAdminPosts(1, 200)
|
||||
])
|
||||
codeTypeOptions.value = types.map(t => ({ value: t.id, label: `${t.name} (${t.category === 0 ? '前端' : t.category === 1 ? '后端' : '其他'})` }))
|
||||
adminPosts.value = postsRes.list || []
|
||||
} catch (error) {
|
||||
console.error('Failed to load code types:', error)
|
||||
console.error('Failed to load form data:', error)
|
||||
}
|
||||
|
||||
if (isEditing.value) {
|
||||
@@ -186,6 +250,7 @@ onMounted(async () => {
|
||||
form.code = snippet.code
|
||||
form.codeTypeId = snippet.codeTypeId || snippet.codeType?.id || 0
|
||||
form.description = snippet.description || ''
|
||||
form.postIds = snippet.posts?.map(p => p.id) || []
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch snippet data:', error)
|
||||
toast.error('加载代码片段数据失败: ' + (error.message || '未知错误'))
|
||||
@@ -195,147 +260,66 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.snippet-form-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: #d4b383;
|
||||
margin-bottom: 1.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.snippet-form {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
font-family: 'Inter', sans-serif;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group input::placeholder,
|
||||
.form-group textarea::placeholder,
|
||||
.form-group select::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #d4b383;
|
||||
box-shadow: 0 0 0 3px rgba(212, 179, 131, 0.2);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: rgba(212, 179, 131, 0.1);
|
||||
border-color: #d4b383;
|
||||
color: #d4b383;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background-color: #d4b383;
|
||||
color: #050505;
|
||||
border: 1px solid #d4b383;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background-color: transparent;
|
||||
color: #d4b383;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0 15px rgba(212, 179, 131, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* MdEditor Customization (Consistent with PostForm) */
|
||||
.editor-wrapper {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-wrapper:focus-within {
|
||||
border-color: #d4b383;
|
||||
box-shadow: 0 0 0 3px rgba(212, 179, 131, 0.2);
|
||||
}
|
||||
|
||||
:deep(.custom-md-editor) {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
background-color: transparent !important;
|
||||
--md-bk-color: transparent !important;
|
||||
--md-color: white !important;
|
||||
--md-border-color: transparent !important;
|
||||
height: 500px;
|
||||
height: 100% !important;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
:deep(.desc-md-editor) {
|
||||
height: 100% !important;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.post-chip-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.post-chip {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.post-chip:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.post-chip--active {
|
||||
background: rgba(212, 179, 131, 0.15);
|
||||
border-color: #d4b383;
|
||||
color: #d4b383;
|
||||
}
|
||||
|
||||
:deep(.md-editor-toolbar-wrapper) {
|
||||
@@ -351,20 +335,4 @@ onMounted(async () => {
|
||||
background-color: rgba(0, 0, 0, 0.2) !important;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.form-container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -63,7 +63,7 @@ const fetchSnippets = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSnippet = async (id: string) => {
|
||||
const deleteSnippet = async (id: number) => {
|
||||
if (confirm('确定要删除这个代码片段吗?')) {
|
||||
try {
|
||||
await deleteSnippetApi(id)
|
||||
|
||||
Reference in New Issue
Block a user