合作页面的内容
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<div class="absolute -top-20 -left-20 w-64 h-64 bg-art-accent/10 rounded-full blur-[80px]"></div>
|
||||
<div class="relative z-10">
|
||||
<h3 class="font-serif text-3xl text-white italic mb-6 leading-tight">
|
||||
Let's build<br>something<br>unreal.
|
||||
共创<br>无限<br>可能
|
||||
</h3>
|
||||
<p class="font-mono text-xs text-white/40 leading-relaxed">
|
||||
告诉我你的构想。<br>
|
||||
@@ -36,7 +36,10 @@
|
||||
placeholder="Name"
|
||||
v-model="formData.name"
|
||||
>
|
||||
<label class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-not-placeholder-shown:-top-4 peer-not-placeholder-shown:text-white/50 pointer-events-none">您的称呼</label>
|
||||
<label
|
||||
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-autofill:-top-4 peer-autofill:text-art-accent pointer-events-none"
|
||||
:class="formData.name ? '-top-4 text-white/50' : ''"
|
||||
>您的称呼</label>
|
||||
</div>
|
||||
<div class="relative group/input">
|
||||
<input
|
||||
@@ -46,7 +49,10 @@
|
||||
placeholder="Company"
|
||||
v-model="formData.company"
|
||||
>
|
||||
<label class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-not-placeholder-shown:-top-4 peer-not-placeholder-shown:text-white/50 pointer-events-none">公司 / 组织</label>
|
||||
<label
|
||||
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-autofill:-top-4 peer-autofill:text-art-accent pointer-events-none"
|
||||
:class="formData.company ? '-top-4 text-white/50' : ''"
|
||||
>公司 / 组织</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative group/input">
|
||||
@@ -57,7 +63,10 @@
|
||||
placeholder="Contact"
|
||||
v-model="formData.contact"
|
||||
>
|
||||
<label class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-not-placeholder-shown:-top-4 peer-not-placeholder-shown:text-white/50 pointer-events-none">联系方式 (Email / WeChat)</label>
|
||||
<label
|
||||
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-autofill:-top-4 peer-autofill:text-art-accent pointer-events-none"
|
||||
:class="formData.contact ? '-top-4 text-white/50' : ''"
|
||||
>联系方式 (Email / WeChat)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
@@ -73,7 +82,7 @@
|
||||
class="peer hidden"
|
||||
v-model="formData.budget"
|
||||
>
|
||||
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">10k - 50k</span>
|
||||
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">1w - 5w</span>
|
||||
</label>
|
||||
<label class="cursor-pointer group">
|
||||
<input
|
||||
@@ -83,7 +92,7 @@
|
||||
class="peer hidden"
|
||||
v-model="formData.budget"
|
||||
>
|
||||
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">50k - 200k</span>
|
||||
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">5w - 20w</span>
|
||||
</label>
|
||||
<label class="cursor-pointer group">
|
||||
<input
|
||||
@@ -93,7 +102,7 @@
|
||||
class="peer hidden"
|
||||
v-model="formData.budget"
|
||||
>
|
||||
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">200k +</span>
|
||||
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">20w +</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,7 +115,10 @@
|
||||
v-model="formData.description"
|
||||
@input="autoResizeTextarea"
|
||||
></textarea>
|
||||
<label class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-not-placeholder-shown:-top-4 peer-not-placeholder-shown:text-white/50 pointer-events-none">一句话描述需求</label>
|
||||
<label
|
||||
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-autofill:-top-4 peer-autofill:text-art-accent pointer-events-none"
|
||||
:class="formData.description ? '-top-4 text-white/50' : ''"
|
||||
>一句话描述需求</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
|
||||
@@ -45,10 +45,22 @@
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/admin/about" class="nav-link" active-class="active">
|
||||
<span class="nav-icon"><EFBFBD></span>
|
||||
<span class="nav-icon">👤</span>
|
||||
<span class="nav-text">关于页面</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/admin/testimonials" class="nav-link" active-class="active">
|
||||
<span class="nav-icon">💬</span>
|
||||
<span class="nav-text">客户评价</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/admin/partners" class="nav-link" active-class="active">
|
||||
<span class="nav-icon">🤝</span>
|
||||
<span class="nav-text">合作伙伴</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/admin/snippets" class="nav-link" active-class="active">
|
||||
<span class="nav-icon">💻</span>
|
||||
|
||||
@@ -157,19 +157,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeMount, onUpdated, nextTick } from 'vue'
|
||||
import { useScrollAnimation } from '../composables/useScrollAnimation'
|
||||
|
||||
// 定义类型
|
||||
interface Testimonial {
|
||||
id: string
|
||||
content: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
interface Partner {
|
||||
id: string
|
||||
name: string
|
||||
logo?: string
|
||||
}
|
||||
import {
|
||||
fetchTestimonials,
|
||||
fetchPartners,
|
||||
type Testimonial,
|
||||
type Partner
|
||||
} from '../services/api'
|
||||
|
||||
const testimonials = ref<Testimonial[]>([])
|
||||
const partners = ref<Partner[]>([])
|
||||
@@ -182,48 +175,20 @@ const refreshIcons = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟API调用,实际项目中应替换为真实API
|
||||
// 获取服务数据
|
||||
const fetchServicesData = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
// 实际项目中应调用真实API
|
||||
// const testimonialsRes = await fetch(`${API_BASE}/testimonials`)
|
||||
// const partnersRes = await fetch(`${API_BASE}/partners`)
|
||||
// testimonials.value = await testimonialsRes.json()
|
||||
// partners.value = await partnersRes.json()
|
||||
|
||||
// 使用模拟数据作为备选
|
||||
testimonials.value = [
|
||||
{ id: '1', content: '从未见过如此丝滑的 WebGL 体验!', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=test1' },
|
||||
{ id: '2', content: '代码质量非常高,易于维护。', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=test2' },
|
||||
{ id: '3', content: '细节把控令人惊叹,强烈推荐!', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=test3' },
|
||||
{ id: '4', content: '交付速度快,超出预期!', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=test4' }
|
||||
]
|
||||
|
||||
partners.value = [
|
||||
{ id: '1', name: 'VOGUE' },
|
||||
{ id: '2', name: 'WIRED' },
|
||||
{ id: '3', name: 'stripe' },
|
||||
{ id: '4', name: 'Monocle' }
|
||||
]
|
||||
const [testimonialsData, partnersData] = await Promise.all([
|
||||
fetchTestimonials(),
|
||||
fetchPartners()
|
||||
])
|
||||
testimonials.value = testimonialsData
|
||||
partners.value = partnersData
|
||||
} catch (err) {
|
||||
console.error('Error fetching services data:', err)
|
||||
error.value = '获取服务数据失败,请稍后重试'
|
||||
// 使用静态数据作为备选
|
||||
testimonials.value = [
|
||||
{ id: '1', content: '从未见过如此丝滑的 WebGL 体验!', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=test1' },
|
||||
{ id: '2', content: '代码质量非常高,易于维护。', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=test2' },
|
||||
{ id: '3', content: '细节把控令人惊叹,强烈推荐!', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=test3' },
|
||||
{ id: '4', content: '交付速度快,超出预期!', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=test4' }
|
||||
]
|
||||
|
||||
partners.value = [
|
||||
{ id: '1', name: 'VOGUE' },
|
||||
{ id: '2', name: 'WIRED' },
|
||||
{ id: '3', name: 'stripe' },
|
||||
{ id: '4', name: 'Monocle' }
|
||||
]
|
||||
} finally {
|
||||
loading.value = false
|
||||
nextTick(refreshIcons)
|
||||
|
||||
196
client/src/pages/admin/PartnerForm.vue
Normal file
196
client/src/pages/admin/PartnerForm.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="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 type="text" v-model="formData.name" class="form-input" required placeholder="请输入合作伙伴名称">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Logo URL</label>
|
||||
<input type="text" v-model="formData.logo" class="form-input" required placeholder="https://...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">官网链接</label>
|
||||
<input type="text" v-model="formData.website" class="form-input" placeholder="https://...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">描述</label>
|
||||
<textarea v-model="formData.description" class="form-input" rows="4" placeholder="请输入描述"></textarea>
|
||||
</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 { useToast } from '../../composables/useToast'
|
||||
import { createPartner, updatePartner, fetchPartners } from '../../services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
logo: '',
|
||||
website: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
if (!isEdit.value) return
|
||||
|
||||
try {
|
||||
const id = parseInt(route.params.id as string)
|
||||
// Same as testimonials, fetching list to find item
|
||||
const list = await fetchPartners()
|
||||
const item = list.find(p => p.id === id)
|
||||
if (item) {
|
||||
formData.value = {
|
||||
name: item.name,
|
||||
logo: item.logo,
|
||||
website: item.website,
|
||||
description: item.description
|
||||
}
|
||||
} else {
|
||||
toast.error('未找到该合作伙伴')
|
||||
router.push('/admin/partners')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load partner:', error)
|
||||
toast.error('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updatePartner(parseInt(route.params.id as string), formData.value)
|
||||
toast.success('更新成功')
|
||||
} else {
|
||||
await createPartner(formData.value)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
router.push('/admin/partners')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to submit form:', error)
|
||||
toast.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: #d4b383;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.375rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #d4b383;
|
||||
box-shadow: 0 0 0 3px rgba(212, 179, 131, 0.1);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: rgba(212, 179, 131, 0.1);
|
||||
border: 1px solid #d4b383;
|
||||
color: #d4b383;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: rgba(212, 179, 131, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(212, 179, 131, 0.15);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
232
client/src/pages/admin/Partners.vue
Normal file
232
client/src/pages/admin/Partners.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="partners-container">
|
||||
<h1 class="page-title">合作伙伴管理</h1>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-bar">
|
||||
<button class="btn-primary" @click="router.push('/admin/partners/create')">
|
||||
<span class="btn-icon">➕</span>
|
||||
新建合作伙伴
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Partners Table -->
|
||||
<div class="table-container">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Logo</th>
|
||||
<th>名称</th>
|
||||
<th>网站</th>
|
||||
<th>描述</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="p in partners" :key="p.id">
|
||||
<td class="table-cell">{{ p.id }}</td>
|
||||
<td class="table-cell">
|
||||
<img v-if="p.logo" :src="p.logo" alt="Logo" class="w-8 h-8 object-contain bg-white/10 rounded">
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td class="table-cell">{{ p.name }}</td>
|
||||
<td class="table-cell">
|
||||
<a v-if="p.website" :href="p.website" target="_blank" class="text-blue-400 hover:underline">链接</a>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td class="table-cell" :title="p.description">
|
||||
{{ p.description && p.description.length > 20 ? p.description.substring(0, 20) + '...' : (p.description || '-') }}
|
||||
</td>
|
||||
<td class="table-cell">{{ new Date(p.createdAt).toLocaleDateString() }}</td>
|
||||
<td class="table-cell actions">
|
||||
<button class="btn-edit" @click="router.push(`/admin/partners/${p.id}/edit`)" title="编辑">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-delete" @click="handleDelete(p.id)" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="partners.length === 0" class="empty-state">
|
||||
<p>暂无合作伙伴数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { fetchPartners, deletePartner, type Partner } from '../../services/api'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
const partners = ref<Partner[]>([])
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
partners.value = await fetchPartners()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch partners:', error)
|
||||
toast.error('获取合作伙伴列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (confirm('确定要删除这个合作伙伴吗?')) {
|
||||
try {
|
||||
await deletePartner(id)
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete partner:', error)
|
||||
toast.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.partners-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: #d4b383;
|
||||
margin-bottom: 1.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: rgba(212, 179, 131, 0.1);
|
||||
border: 1px solid #d4b383;
|
||||
color: #d4b383;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: rgba(212, 179, 131, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(212, 179, 131, 0.15);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.admin-table thead {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #d4b383;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.admin-table td {
|
||||
padding: 1rem;
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.admin-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.admin-table tr:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-edit, .btn-delete {
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: rgba(59, 130, 246, 0.8);
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: rgba(239, 68, 68, 0.8);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
205
client/src/pages/admin/TestimonialForm.vue
Normal file
205
client/src/pages/admin/TestimonialForm.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="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 type="text" v-model="formData.author" class="form-input" required placeholder="请输入作者姓名">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">角色/职位</label>
|
||||
<input type="text" v-model="formData.role" class="form-input" placeholder="例如:CEO, Tech Lead">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">头像 URL</label>
|
||||
<input type="text" v-model="formData.avatar" class="form-input" placeholder="https://...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">评分 (1-5)</label>
|
||||
<input type="number" v-model.number="formData.rating" class="form-input" min="1" max="5" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">评价内容</label>
|
||||
<textarea v-model="formData.content" class="form-input" rows="4" required placeholder="请输入评价内容"></textarea>
|
||||
</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 { useToast } from '../../composables/useToast'
|
||||
import { createTestimonial, updateTestimonial, fetchTestimonials } from '../../services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
|
||||
const formData = ref({
|
||||
author: '',
|
||||
role: '',
|
||||
avatar: '',
|
||||
rating: 5,
|
||||
content: ''
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
if (!isEdit.value) return
|
||||
|
||||
try {
|
||||
const id = parseInt(route.params.id as string)
|
||||
// Since we don't have a single fetch API for admin yet (or we can use the list),
|
||||
// we'll fetch all and find one. In a real app, we should have getTestimonial(id).
|
||||
// But for now, let's fetch list and find.
|
||||
const list = await fetchTestimonials()
|
||||
const item = list.find(t => t.id === id)
|
||||
if (item) {
|
||||
formData.value = {
|
||||
author: item.author,
|
||||
role: item.role,
|
||||
avatar: item.avatar,
|
||||
rating: item.rating,
|
||||
content: item.content
|
||||
}
|
||||
} else {
|
||||
toast.error('未找到该评价')
|
||||
router.push('/admin/testimonials')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load testimonial:', error)
|
||||
toast.error('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateTestimonial(parseInt(route.params.id as string), formData.value)
|
||||
toast.success('更新成功')
|
||||
} else {
|
||||
await createTestimonial(formData.value)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
router.push('/admin/testimonials')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to submit form:', error)
|
||||
toast.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: #d4b383;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.375rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #d4b383;
|
||||
box-shadow: 0 0 0 3px rgba(212, 179, 131, 0.1);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: rgba(212, 179, 131, 0.1);
|
||||
border: 1px solid #d4b383;
|
||||
color: #d4b383;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: rgba(212, 179, 131, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(212, 179, 131, 0.15);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
226
client/src/pages/admin/Testimonials.vue
Normal file
226
client/src/pages/admin/Testimonials.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="testimonials-container">
|
||||
<h1 class="page-title">客户评价管理</h1>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-bar">
|
||||
<button class="btn-primary" @click="router.push('/admin/testimonials/create')">
|
||||
<span class="btn-icon">➕</span>
|
||||
新建评价
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Testimonials Table -->
|
||||
<div class="table-container">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>作者</th>
|
||||
<th>角色</th>
|
||||
<th>内容</th>
|
||||
<th>评分</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="t in testimonials" :key="t.id">
|
||||
<td class="table-cell">{{ t.id }}</td>
|
||||
<td class="table-cell">{{ t.author }}</td>
|
||||
<td class="table-cell">{{ t.role }}</td>
|
||||
<td class="table-cell" :title="t.content">
|
||||
{{ t.content.length > 30 ? t.content.substring(0, 30) + '...' : t.content }}
|
||||
</td>
|
||||
<td class="table-cell">{{ t.rating }}</td>
|
||||
<td class="table-cell">{{ new Date(t.createdAt).toLocaleDateString() }}</td>
|
||||
<td class="table-cell actions">
|
||||
<button class="btn-edit" @click="router.push(`/admin/testimonials/${t.id}/edit`)" title="编辑">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-delete" @click="handleDelete(t.id)" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="testimonials.length === 0" class="empty-state">
|
||||
<p>暂无客户评价数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { fetchTestimonials, deleteTestimonial, type Testimonial } from '../../services/api'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
const testimonials = ref<Testimonial[]>([])
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
testimonials.value = await fetchTestimonials()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch testimonials:', error)
|
||||
toast.error('获取评价列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (confirm('确定要删除这条评价吗?')) {
|
||||
try {
|
||||
await deleteTestimonial(id)
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete testimonial:', error)
|
||||
toast.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.testimonials-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: #d4b383;
|
||||
margin-bottom: 1.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: rgba(212, 179, 131, 0.1);
|
||||
border: 1px solid #d4b383;
|
||||
color: #d4b383;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: rgba(212, 179, 131, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(212, 179, 131, 0.15);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.admin-table thead {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #d4b383;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.admin-table td {
|
||||
padding: 1rem;
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.admin-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.admin-table tr:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-edit, .btn-delete {
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: rgba(59, 130, 246, 0.8);
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: rgba(239, 68, 68, 0.8);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -58,6 +58,16 @@ const routes = [
|
||||
{ path: 'about/create', name: 'admin-about-create', component: () => import('./pages/admin/AboutForm.vue') },
|
||||
{ path: 'about/edit/:id', name: 'admin-about-edit', component: () => import('./pages/admin/AboutForm.vue') },
|
||||
|
||||
// 客户评价管理
|
||||
{ path: 'testimonials', name: 'admin-testimonials', component: () => import('./pages/admin/Testimonials.vue') },
|
||||
{ path: 'testimonials/create', name: 'admin-testimonials-create', component: () => import('./pages/admin/TestimonialForm.vue') },
|
||||
{ path: 'testimonials/:id/edit', name: 'admin-testimonials-edit', component: () => import('./pages/admin/TestimonialForm.vue') },
|
||||
|
||||
// 合作伙伴管理
|
||||
{ path: 'partners', name: 'admin-partners', component: () => import('./pages/admin/Partners.vue') },
|
||||
{ path: 'partners/create', name: 'admin-partners-create', component: () => import('./pages/admin/PartnerForm.vue') },
|
||||
{ path: 'partners/:id/edit', name: 'admin-partners-edit', component: () => import('./pages/admin/PartnerForm.vue') },
|
||||
|
||||
// 系统配置
|
||||
{ path: 'settings', name: 'admin-settings', component: () => import('./pages/admin/Settings.vue') },
|
||||
|
||||
|
||||
@@ -961,3 +961,156 @@ export const deleteAboutProfile = async (id: number): Promise<void> => {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 客户评价相关类型
|
||||
export interface Testimonial {
|
||||
id: number
|
||||
content: string
|
||||
author: string
|
||||
role: string
|
||||
avatar: string
|
||||
rating: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 合作伙伴相关类型
|
||||
export interface Partner {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
description: string
|
||||
website: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 客户评价API
|
||||
export const fetchTestimonials = async (): Promise<Testimonial[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/testimonials`)
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '获取客户评价失败')
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Fetch testimonials error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const createTestimonial = async (data: Omit<Testimonial, 'id' | 'createdAt' | 'updatedAt'>): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/admin/testimonials`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '创建客户评价失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Create testimonial error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updateTestimonial = async (id: number, data: Omit<Testimonial, 'id' | 'createdAt' | 'updatedAt'>): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/admin/testimonials/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '更新客户评价失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Update testimonial error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteTestimonial = async (id: number): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/admin/testimonials/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '删除客户评价失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete testimonial error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 合作伙伴API
|
||||
export const fetchPartners = async (): Promise<Partner[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/partners`)
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '获取合作伙伴失败')
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Fetch partners error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const createPartner = async (data: Omit<Partner, 'id' | 'createdAt' | 'updatedAt'>): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/admin/partners`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '创建合作伙伴失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Create partner error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updatePartner = async (id: number, data: Omit<Partner, 'id' | 'createdAt' | 'updatedAt'>): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/admin/partners/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '更新合作伙伴失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Update partner error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const deletePartner = async (id: number): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/admin/partners/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '删除合作伙伴失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete partner error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user