Files
jczxw-pc/app/pages/developer/support.vue
2026-04-23 16:30:57 +08:00

406 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dev-page">
<div class="page-header">
<div>
<h2 class="page-title">💬 支持与反馈</h2>
<p class="page-desc">遇到问题查看常见问题或联系我们的技术支持团队</p>
</div>
</div>
<div class="page-body">
<!-- 联系渠道 -->
<a-row :gutter="[16, 16]" class="mb-5">
<a-col :xs="24" :md="8" v-for="channel in channels" :key="channel.title">
<div class="channel-card" @click="handleChannelClick(channel)">
<div class="channel-icon">{{ channel.icon }}</div>
<div class="channel-title">{{ channel.title }}</div>
<div class="channel-desc">{{ channel.desc }}</div>
<div class="channel-action">{{ channel.action }} </div>
</div>
</a-col>
</a-row>
<a-row :gutter="[16, 0]">
<!-- FAQ 常见问题 -->
<a-col :xs="24" :lg="15">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 常见问题</span>
<a-input-search
v-model:value="faqSearch"
placeholder="搜索问题..."
style="width: 180px"
size="small"
/>
</div>
<div class="faq-list">
<a-collapse v-model:activeKey="activeKeys" :bordered="false" ghost>
<a-collapse-panel
v-for="faq in filteredFaqs"
:key="faq.key"
:header="faq.question"
class="faq-panel"
>
<p class="faq-answer">{{ faq.answer }}</p>
<div v-if="faq.link" class="faq-link" @click="navigateTo(faq.link.to)">
📎 {{ faq.link.label }}
</div>
</a-collapse-panel>
</a-collapse>
<div v-if="filteredFaqs.length === 0" class="empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">没有找到相关问题</div>
</div>
</div>
</div>
</a-col>
<!-- 提交工单 & 状态 -->
<a-col :xs="24" :lg="9">
<div class="panel">
<div class="panel-header">
<span class="panel-title">🎫 提交工单</span>
</div>
<div class="ticket-form">
<a-form layout="vertical">
<a-form-item label="问题类型">
<a-select v-model:value="ticketForm.type" placeholder="选择问题类型">
<a-select-option value="api">API 接口问题</a-select-option>
<a-select-option value="sdk">SDK 使用问题</a-select-option>
<a-select-option value="source">源码权限问题</a-select-option>
<a-select-option value="deploy">部署运维问题</a-select-option>
<a-select-option value="other">其他问题</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="问题描述">
<a-textarea
v-model:value="ticketForm.content"
:rows="4"
placeholder="详细描述你遇到的问题..."
:maxlength="500"
show-count
/>
</a-form-item>
<a-form-item label="联系方式(可选)">
<a-input
v-model:value="ticketForm.contact"
placeholder="邮箱或微信,方便我们回复你"
/>
</a-form-item>
<a-button
type="primary"
block
:loading="submitting"
@click="handleSubmitTicket"
>
提交工单
</a-button>
</a-form>
</div>
</div>
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">📊 服务状态</span>
<a-tag color="green"> 全部正常</a-tag>
</div>
<div class="status-list">
<div v-for="srv in serviceStatus" :key="srv.name" class="srv-item">
<div class="srv-indicator" :class="srv.status" />
<span class="srv-name">{{ srv.name }}</span>
<span class="srv-latency">{{ srv.latency }}</span>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
definePageMeta({ layout: 'developer' })
useHead({ title: '支持与反馈 - 开发者中心' })
const faqSearch = ref('')
const activeKeys = ref<string[]>([])
const submitting = ref(false)
const ticketForm = reactive({
type: undefined as string | undefined,
content: '',
contact: '',
})
const channels = [
{
icon: '💬',
title: '开发者论坛',
desc: '与其他开发者交流,分享经验与解决方案',
action: '进入论坛',
type: 'link',
url: 'https://forum.websoft.top',
},
{
icon: '📘',
title: '技术文档',
desc: '完整 API 参考、教程与最佳实践文档库',
action: '查看文档',
type: 'route',
to: '/developer-center',
},
{
icon: '🤝',
title: '企业技术支持',
desc: '专属技术顾问,工作日 9:00-18:00 在线响应',
action: '立即联系',
type: 'route',
to: '/contact',
},
]
function handleChannelClick(channel: any) {
if (channel.type === 'link') {
if (import.meta.client) window.open(channel.url, '_blank', 'noopener,noreferrer')
} else {
navigateTo(channel.to)
}
}
const faqs = [
{
key: '1',
question: '如何获取 API Key',
answer: '登录开发者中心后,进入"API Key 管理"页面,点击"创建 API Key"按钮即可生成。建议根据用途创建不同的 Key便于管理和权限控制。',
link: { label: '前往 API Key 管理', to: '/developer/apikeys' },
},
{
key: '2',
question: 'API 请求返回 401 未授权怎么处理?',
answer: '请检查1) Authorization 请求头格式是否为 "Bearer sk-xxxx"2) API Key 是否已被禁用或过期3) 请求的接口是否在 Key 的权限范围内。',
link: null,
},
{
key: '3',
question: '如何申请源码仓库访问权限?',
answer: '需要完成以下步骤:① 在 Gitea 注册账号;② 在"Git 账号绑定"页面填写用户名;③ 在"权限申请记录"提交申请;④ 等待运营审核1-3工作日。',
link: { label: '前往 Git 账号绑定', to: '/developer/git' },
},
{
key: '4',
question: 'TypeScript SDK 如何安装和初始化?',
answer: '通过 npm install @websopy/sdk 安装,然后 import { WebsopyClient } from "@websopy/sdk",使用 new WebsopyClient({ apiKey: "sk-xxx" }) 初始化即可。',
link: { label: '查看快速开始教程', to: '/developer/docs/getting-started/quickstart' },
},
{
key: '5',
question: '接口速率限制是多少?',
answer: '免费版5次/秒日限1000次基础版20次/秒日限10000次专业版100次/秒日限100000次企业版可自定义。超出限制会返回 429 Too Many Requests。',
link: null,
},
{
key: '6',
question: 'AI 流式响应SSE如何接入',
answer: '在 agent.chat() 方法中传入 stream: true 参数,返回值为 AsyncIterable遍历即可逐步获取 AI 输出内容。详见流式输出教程。',
link: { label: '查看流式输出教程', to: '/developer/docs/api/streaming' },
},
]
const filteredFaqs = computed(() => {
const kw = faqSearch.value.trim().toLowerCase()
if (!kw) return faqs
return faqs.filter(f =>
f.question.toLowerCase().includes(kw) || f.answer.toLowerCase().includes(kw)
)
})
async function handleSubmitTicket() {
if (!ticketForm.content.trim()) {
message.error('请填写问题描述')
return
}
submitting.value = true
await new Promise(r => setTimeout(r, 800))
submitting.value = false
message.success('工单已提交,我们将尽快回复')
Object.assign(ticketForm, { type: undefined, content: '', contact: '' })
}
const serviceStatus = [
{ name: 'REST API', status: 'ok', latency: '28ms' },
{ name: 'AI Agent API', status: 'ok', latency: '312ms' },
{ name: 'Gitea 仓库', status: 'ok', latency: '45ms' },
{ name: 'SDK CDN', status: 'ok', latency: '18ms' },
{ name: 'Webhook 推送', status: 'ok', latency: '—' },
]
</script>
<style scoped>
.dev-page { min-height: 100%; }
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 24px 28px 16px;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.page-title {
font-size: 20px;
font-weight: 700;
color: rgba(0, 0, 0, 0.88);
margin: 0 0 4px;
}
.page-desc {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin: 0;
}
.page-body {
padding: 20px 24px 28px;
}
/* 联系渠道卡片 */
.channel-card {
padding: 20px;
border-radius: 12px;
border: 1px solid #f0f0f0;
background: #fff;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.channel-card:hover {
border-color: #c7d2fe;
box-shadow: 0 4px 16px rgba(79, 70, 229, 0.1);
transform: translateY(-2px);
}
.channel-icon { font-size: 36px; margin-bottom: 10px; }
.channel-title { font-size: 15px; font-weight: 700; color: rgba(0, 0, 0, 0.85); margin-bottom: 6px; }
.channel-desc { font-size: 13px; color: rgba(0, 0, 0, 0.45); margin-bottom: 12px; line-height: 1.5; }
.channel-action { font-size: 13px; color: #4f46e5; font-weight: 500; }
/* 面板通用 */
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
/* FAQ */
.faq-list {
padding: 8px 6px;
}
.faq-panel {
border-bottom: 1px solid #f5f5f5 !important;
}
:deep(.ant-collapse-header) {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.8) !important;
padding: 14px 16px !important;
}
:deep(.ant-collapse-content-box) {
padding: 0 16px 14px !important;
}
.faq-answer {
font-size: 13px;
color: rgba(0, 0, 0, 0.55);
line-height: 1.7;
margin: 0 0 8px;
}
.faq-link {
font-size: 12px;
color: #4f46e5;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
.faq-link:hover { text-decoration: underline; }
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 36px 24px;
text-align: center;
}
.empty-icon { font-size: 36px; margin-bottom: 10px; }
.empty-title { font-size: 14px; color: rgba(0, 0, 0, 0.5); }
/* 工单表单 */
.ticket-form {
padding: 16px 18px;
}
/* 服务状态 */
.status-list {
padding: 10px 16px;
}
.srv-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid #f9f9f9;
}
.srv-item:last-child { border-bottom: none; }
.srv-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.srv-indicator.ok { background: #16a34a; }
.srv-indicator.warn { background: #f59e0b; }
.srv-indicator.down { background: #dc2626; }
.srv-name {
flex: 1;
font-size: 13px;
color: rgba(0, 0, 0, 0.72);
}
.srv-latency {
font-size: 12px;
color: rgba(0, 0, 0, 0.38);
font-family: monospace;
}
</style>