增加分享码功能

This commit is contained in:
2025-12-03 07:57:18 +08:00
parent 5c4798cdc9
commit 0ee688582a
3 changed files with 288 additions and 0 deletions
+276
View File
@@ -319,6 +319,126 @@ function importPresets(event: Event) {
(event.target as HTMLInputElement).value = '';
}
// Share & Cloud
const showShareDialog = ref(false);
const shareTab = ref<'create' | 'import'>('create');
const shareLoading = ref(false);
const shareResultCode = ref('');
const shareImportCode = ref('');
const shareSinglePreset = ref<ExtendedPreset | null>(null);
function openShareDialog(preset?: ExtendedPreset) {
shareSinglePreset.value = preset || null;
shareTab.value = 'create';
shareResultCode.value = '';
shareImportCode.value = '';
showShareDialog.value = true;
}
function handleShare(preset: ExtendedPreset) {
openShareDialog(preset);
}
async function generateShareCode() {
shareLoading.value = true;
try {
let data;
let type = 'all';
if (shareSinglePreset.value) {
data = shareSinglePreset.value;
type = 'single';
} else {
const jsonString = store.exportPresetsToJson();
try {
data = JSON.parse(jsonString);
} catch (e) {
data = {};
}
}
const response = await fetch('https://sywb.top/api/share/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ data, type })
});
const result = await response.json();
if (result.success) {
shareResultCode.value = result.code;
showNotification('分享码生成成功', 'success');
} else {
showNotification(result.error || '生成失败', 'error');
}
} catch (error) {
console.error(error);
showNotification('网络错误,无法生成分享码', 'error');
} finally {
shareLoading.value = false;
}
}
async function importFromShareCode() {
if (!shareImportCode.value || shareImportCode.value.length !== 6) {
showNotification('请输入有效的6位分享码', 'error');
return;
}
shareLoading.value = true;
try {
const response = await fetch(`https://sywb.top/api/share/${shareImportCode.value}`);
const result = await response.json();
if (result.success) {
const data = result.data;
if (result.type === 'single') {
// Import single preset
const newPreset = { ...data };
// Remove ID to create new
if (newPreset.id) delete newPreset.id;
// Ensure name uniqueness or mark as imported
newPreset.name = newPreset.name + ' (Imported)';
store.createExtendedPreset(newPreset);
showNotification(`预设「${newPreset.name}」导入成功`, 'success');
} else {
// Import all
const jsonString = JSON.stringify(data);
const success = store.importPresetsFromJson(jsonString);
if (success) {
showNotification('预设导入成功', 'success');
} else {
showNotification('导入数据格式错误', 'error');
}
}
closeShareDialog();
} else {
showNotification(result.error || '分享码无效或已过期', 'error');
}
} catch (error) {
console.error(error);
showNotification('网络错误,无法导入', 'error');
} finally {
shareLoading.value = false;
}
}
function copyShareCode() {
navigator.clipboard.writeText(shareResultCode.value);
showNotification('分享码已复制', 'success');
}
function closeShareDialog() {
showShareDialog.value = false;
shareResultCode.value = '';
shareImportCode.value = '';
shareSinglePreset.value = null;
}
// Helpers
function resetPresetForm() {
presetForm.value = {
@@ -406,6 +526,11 @@ onMounted(() => {
</button>
<div class="import-export">
<button @click="openShareDialog()" class="btn-icon" title="云端分享/导入">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
</svg>
</button>
<button @click="exportPresets" class="btn-icon" title="导出预设">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="currentColor" stroke-width="2"/>
@@ -433,6 +558,7 @@ onMounted(() => {
@edit="editPreset"
@delete="deletePreset"
@copy="copyPresetContent"
@share="handleShare"
/>
</div>
</div>
@@ -542,6 +668,83 @@ onMounted(() => {
</div>
</div>
<!-- Share/Import Modal -->
<div v-if="showShareDialog" class="modal-overlay" @click.self="closeShareDialog">
<div class="modal-content share-modal">
<div class="modal-header">
<h3>云端分享与导入</h3>
<button @click="closeShareDialog" class="close-btn">×</button>
</div>
<div class="share-tabs">
<button
:class="{ active: shareTab === 'create' }"
@click="shareTab = 'create'"
>
创建分享
</button>
<button
:class="{ active: shareTab === 'import' }"
@click="shareTab = 'import'"
>
导入预设
</button>
</div>
<div class="modal-body">
<!-- Create Share -->
<div v-if="shareTab === 'create'" class="share-panel">
<div class="share-info">
<p v-if="shareSinglePreset">
正在分享预设: <strong>{{ shareSinglePreset.name }}</strong>
</p>
<p v-else>
正在分享: <strong>所有预设数据</strong>
</p>
<p class="text-muted">生成一个6位数的分享码有效期24小时</p>
</div>
<div v-if="shareResultCode" class="share-result">
<div class="code-display">{{ shareResultCode }}</div>
<button @click="copyShareCode" class="btn-secondary">复制分享码</button>
</div>
<div v-else class="share-action">
<button
@click="generateShareCode"
class="btn-primary full-width"
:disabled="shareLoading"
>
{{ shareLoading ? '生成中...' : '生成分享码' }}
</button>
</div>
</div>
<!-- Import Share -->
<div v-if="shareTab === 'import'" class="share-panel">
<div class="form-group">
<label>输入6位分享码</label>
<input
v-model="shareImportCode"
placeholder="例如: 123456"
maxlength="6"
class="code-input"
/>
</div>
<div class="share-action">
<button
@click="importFromShareCode"
class="btn-primary full-width"
:disabled="shareLoading || shareImportCode.length !== 6"
>
{{ shareLoading ? '导入中...' : '导入' }}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Toast Notification -->
<NotificationToast
:message="notification.message"
@@ -842,4 +1045,77 @@ onMounted(() => {
border-color: var(--color-text-primary);
box-shadow: 0 0 0 2px var(--color-bg-primary);
}
.share-modal {
max-width: 400px;
}
.share-tabs {
display: flex;
border-bottom: 1px solid var(--color-border);
margin-bottom: 1rem;
}
.share-tabs button {
flex: 1;
padding: 0.75rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--color-text-secondary);
cursor: pointer;
font-weight: 500;
}
.share-tabs button.active {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
}
.share-info {
text-align: center;
margin-bottom: 1.5rem;
color: var(--color-text-primary);
}
.share-info p {
margin: 0.5rem 0;
}
.text-muted {
color: var(--color-text-tertiary);
font-size: 0.875rem;
}
.share-result {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.code-display {
font-size: 2rem;
font-weight: 700;
letter-spacing: 0.25rem;
color: var(--color-accent);
padding: 1rem;
background-color: var(--color-bg-secondary);
border-radius: var(--radius-md);
width: 100%;
text-align: center;
}
.code-input {
font-size: 1.5rem;
text-align: center;
letter-spacing: 0.25rem;
padding: 0.75rem;
}
.full-width {
width: 100%;
justify-content: center;
padding: 0.75rem;
}
</style>
+11
View File
@@ -12,6 +12,7 @@ const emit = defineEmits<{
(e: 'edit', preset: ExtendedPreset): void;
(e: 'delete', preset: ExtendedPreset): void;
(e: 'copy', preset: ExtendedPreset): void;
(e: 'share', preset: ExtendedPreset): void;
}>();
function getTypeIcon(type: PresetType) {
@@ -80,6 +81,16 @@ function formatDate(dateStr: string) {
</svg>
</button>
<div class="dropdown-content">
<button @click="emit('share', preset)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="18" cy="5" r="3"/>
<circle cx="6" cy="12" r="3"/>
<circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
分享
</button>
<button @click="emit('edit', preset)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>