feat(widget): Widget 配置、loader 与右下角悬浮入口
生成可嵌入站点的 loader 与悬浮按钮,支持展开收起和移动端;附独立宿主测试页。
This commit is contained in:
1 parent
556b650c7a
commit
0cc6299d1c
57 files changed
+32086
No files matched your search
@@ -0,0 +1,208 @@
|
||||
<!--
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ClipboardIcon, CheckIcon } from '@heroicons/vue/24/outline'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const props = defineProps<{
|
||||
apiKey: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const copied = ref(false)
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.apiKey)
|
||||
copied.value = true
|
||||
toast.success('已复制!', {
|
||||
description: 'API 密钥已复制到剪贴板',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
} catch (err) {
|
||||
toast.error('复制失败', {
|
||||
description: '请手动复制',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal @close="emit('close')">
|
||||
<template #title>API 密钥已创建</template>
|
||||
<template #content>
|
||||
<div class="api-key-modal">
|
||||
<div class="warning-banner">
|
||||
<strong>⚠️ 请立即保存此密钥!</strong>
|
||||
<p>这是唯一一次显示该 API 密钥,之后将无法再次查看。</p>
|
||||
</div>
|
||||
|
||||
<div class="key-container">
|
||||
<label>API 密钥:</label>
|
||||
<div class="key-display">
|
||||
<code class="api-key">{{ apiKey }}</code>
|
||||
<button
|
||||
class="copy-button"
|
||||
@click="copyToClipboard"
|
||||
:title="copied ? '已复制!' : '复制到剪贴板'"
|
||||
>
|
||||
<CheckIcon v-if="copied" class="icon success" />
|
||||
<ClipboardIcon v-else class="icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<h4>后续步骤:</h4>
|
||||
<ol>
|
||||
<li>复制该 API 密钥并妥善保管</li>
|
||||
<li>用它来验证组件 Token 生成请求</li>
|
||||
<li>将其放入 Authorization 请求头:<code>Bearer {{ apiKey.substring(0, 20) }}...</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" @click="emit('close')">
|
||||
我已保存密钥
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.api-key-modal {
|
||||
padding: var(--space-md);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.warning-banner {
|
||||
background: var(--warning-bg);
|
||||
border: 1px solid var(--warning-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.warning-banner strong {
|
||||
display: block;
|
||||
color: var(--warning-color);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.warning-banner p {
|
||||
margin: 0;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.key-container {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.key-container label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.key-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm);
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.api-key {
|
||||
flex: 1;
|
||||
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
||||
font-size: var(--text-sm);
|
||||
word-break: break-all;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
padding: var(--space-xs);
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background var(--transition-normal);
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.icon.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: var(--info-bg);
|
||||
border: 1px solid var(--info-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.instructions h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--space-sm);
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
.instructions ol {
|
||||
margin: 0;
|
||||
padding-left: var(--space-lg);
|
||||
}
|
||||
|
||||
.instructions li {
|
||||
margin-bottom: var(--space-xs);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.instructions code {
|
||||
background: var(--code-bg);
|
||||
padding: 2px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user