增加分享码功能
This commit is contained in:
@@ -11,6 +11,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
temp/*
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -319,6 +319,126 @@ function importPresets(event: Event) {
|
|||||||
(event.target as HTMLInputElement).value = '';
|
(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
|
// Helpers
|
||||||
function resetPresetForm() {
|
function resetPresetForm() {
|
||||||
presetForm.value = {
|
presetForm.value = {
|
||||||
@@ -406,6 +526,11 @@ onMounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="import-export">
|
<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="导出预设">
|
<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">
|
<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"/>
|
<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"
|
@edit="editPreset"
|
||||||
@delete="deletePreset"
|
@delete="deletePreset"
|
||||||
@copy="copyPresetContent"
|
@copy="copyPresetContent"
|
||||||
|
@share="handleShare"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -542,6 +668,83 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Toast Notification -->
|
||||||
<NotificationToast
|
<NotificationToast
|
||||||
:message="notification.message"
|
:message="notification.message"
|
||||||
@@ -842,4 +1045,77 @@ onMounted(() => {
|
|||||||
border-color: var(--color-text-primary);
|
border-color: var(--color-text-primary);
|
||||||
box-shadow: 0 0 0 2px var(--color-bg-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>
|
</style>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'edit', preset: ExtendedPreset): void;
|
(e: 'edit', preset: ExtendedPreset): void;
|
||||||
(e: 'delete', preset: ExtendedPreset): void;
|
(e: 'delete', preset: ExtendedPreset): void;
|
||||||
(e: 'copy', preset: ExtendedPreset): void;
|
(e: 'copy', preset: ExtendedPreset): void;
|
||||||
|
(e: 'share', preset: ExtendedPreset): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function getTypeIcon(type: PresetType) {
|
function getTypeIcon(type: PresetType) {
|
||||||
@@ -80,6 +81,16 @@ function formatDate(dateStr: string) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-content">
|
<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)">
|
<button @click="emit('edit', preset)">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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"/>
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user