1.9+
This commit is contained in:
@@ -8,9 +8,6 @@ export const friendsApi = {
|
||||
sendRequest: (toUserId: string, message?: string) =>
|
||||
api.post('/friends/request', { to_user_id: toUserId, message }),
|
||||
|
||||
addDirect: (toUserId: string) =>
|
||||
api.post('/friends/add-direct', { to_user_id: toUserId }),
|
||||
|
||||
acceptRequest: (requestId: string) =>
|
||||
api.put(`/friends/request/${requestId}/accept`),
|
||||
|
||||
|
||||
@@ -13,29 +13,64 @@
|
||||
<span class="result-bio">{{ user.bio || '这个人很懒,什么都没写' }}</span>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<n-button type="primary" size="small" @click="addDirect(user.id)">添加</n-button>
|
||||
<n-button size="small" @click="sendRequest(user.id)">发请求</n-button>
|
||||
<n-button type="primary" size="small" :disabled="user.is_friend || user.request_sent" @click="openRequest(user)">
|
||||
{{ user.is_friend ? '已是好友' : user.request_sent ? '已发送' : '加为好友' }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="searched" class="empty">
|
||||
<p style="color: var(--color-text-hint)">没有找到匹配的用户</p>
|
||||
</div>
|
||||
|
||||
<!-- 发送好友请求弹窗(需对方同意) -->
|
||||
<div v-if="requestTarget" class="modal-overlay" @click.self="requestTarget = null">
|
||||
<div class="request-modal">
|
||||
<div class="modal-header">
|
||||
<h3>添加好友</h3>
|
||||
<span class="close-btn" @click="requestTarget = null">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="target-preview">
|
||||
<n-avatar :size="40" round :style="{ background: 'var(--color-primary)' }">
|
||||
{{ (requestTarget.nickname || requestTarget.username)[0] }}
|
||||
</n-avatar>
|
||||
<div>
|
||||
<div class="target-name">{{ requestTarget.nickname || requestTarget.username }}</div>
|
||||
<div class="target-hint">发送验证消息,对方同意后才能成为好友</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-input v-model:value="verifyMessage" type="textarea" :rows="3"
|
||||
:placeholder="`我是 ${myName},想加你为好友...`" maxlength="100" show-count />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<n-button size="small" @click="requestTarget = null">取消</n-button>
|
||||
<n-button size="small" type="primary" :loading="sending" @click="confirmRequest">发送请求</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import api from '@/api/client'
|
||||
import { friendsApi } from '@/api/friends'
|
||||
|
||||
const message = useMessage()
|
||||
const auth = useAuthStore()
|
||||
const keyword = ref('')
|
||||
const results = ref<any[]>([])
|
||||
const searched = ref(false)
|
||||
const requestTarget = ref<any>(null)
|
||||
const verifyMessage = ref('')
|
||||
const sending = ref(false)
|
||||
let timer: any = null
|
||||
|
||||
const myName = computed(() => auth.user?.nickname || auth.user?.username || '我')
|
||||
|
||||
const debouncedSearch = () => {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(async () => {
|
||||
@@ -47,21 +82,25 @@ const debouncedSearch = () => {
|
||||
}, 400)
|
||||
}
|
||||
|
||||
async function addDirect(userId: string) {
|
||||
try {
|
||||
await friendsApi.addDirect(userId)
|
||||
message.success('已添加为好友')
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data?.detail || '添加失败')
|
||||
}
|
||||
function openRequest(user: any) {
|
||||
requestTarget.value = user
|
||||
verifyMessage.value = `我是 ${myName.value},想加你为好友`
|
||||
}
|
||||
|
||||
async function sendRequest(userId: string) {
|
||||
async function confirmRequest() {
|
||||
if (!requestTarget.value) return
|
||||
sending.value = true
|
||||
try {
|
||||
await friendsApi.sendRequest(userId)
|
||||
message.success('好友请求已发送')
|
||||
await friendsApi.sendRequest(requestTarget.value.id, verifyMessage.value.trim() || undefined)
|
||||
message.success('好友请求已发送,等待对方同意')
|
||||
// 标记该用户已发送请求
|
||||
const r = results.value.find((u) => u.id === requestTarget.value.id)
|
||||
if (r) r.request_sent = true
|
||||
requestTarget.value = null
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data?.detail || '发送失败')
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -80,4 +119,25 @@ async function sendRequest(userId: string) {
|
||||
.result-bio { font-size: 13px; color: var(--color-text-hint); display: block; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.result-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
||||
.empty { text-align: center; padding: 40px; }
|
||||
|
||||
/* 请求弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.4);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 1000;
|
||||
}
|
||||
.request-modal {
|
||||
width: 400px; max-width: 92vw; background: var(--color-surface); border-radius: 16px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.15); overflow: hidden;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 18px 24px; border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.modal-header h3 { margin: 0; font-size: 17px; }
|
||||
.close-btn { cursor: pointer; font-size: 18px; color: var(--color-text-hint); }
|
||||
.modal-body { padding: 20px 24px; display: flex; flex-direction: column; gap: 14px; }
|
||||
.target-preview { display: flex; align-items: center; gap: 12px; }
|
||||
.target-name { font-weight: 600; font-size: 15px; }
|
||||
.target-hint { font-size: 12px; color: var(--color-text-hint); margin-top: 2px; }
|
||||
.modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 24px; border-top: 1px solid var(--color-border); }
|
||||
</style>
|
||||
|
||||
@@ -45,6 +45,47 @@
|
||||
<h3 class="card-title">账号操作</h3>
|
||||
<n-button type="error" ghost block @click="handleLogout">退出登录</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 注销账号 -->
|
||||
<div class="card danger-card">
|
||||
<h3 class="card-title danger-title">🗑️ 注销账号</h3>
|
||||
<p class="danger-desc">注销后账号将永久删除,所有聊天记录、好友、朋友圈、花园数据都会被清除,且<b>不可恢复</b>。</p>
|
||||
<n-button type="error" block @click="showDeleteModal = true">申请注销账号</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注销确认弹窗 -->
|
||||
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
|
||||
<div class="delete-modal">
|
||||
<div class="modal-header">
|
||||
<h3>确认注销账号</h3>
|
||||
<span class="close-btn" @click="showDeleteModal = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="warn-box">
|
||||
⚠️ 此操作<b>不可撤销</b>!你的所有数据将被永久删除:
|
||||
<ul>
|
||||
<li>所有聊天记录与会话</li>
|
||||
<li>好友关系与通讯录</li>
|
||||
<li>朋友圈动态与花园(树/叶子/胶囊)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>为确认,请输入「<b>确认注销</b>」</label>
|
||||
<n-input v-model:value="deleteConfirmText" placeholder="确认注销" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>输入登录密码</label>
|
||||
<n-input v-model:value="deletePassword" type="password" show-password-on="click" placeholder="你的密码" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<n-button @click="showDeleteModal = false">取消</n-button>
|
||||
<n-button type="error" :loading="deleting"
|
||||
:disabled="deleteConfirmText !== '确认注销' || !deletePassword"
|
||||
@click="confirmDelete">永久注销</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -62,6 +103,10 @@ const message = useMessage()
|
||||
|
||||
const changingPwd = ref(false)
|
||||
const changingEmail = ref(false)
|
||||
const showDeleteModal = ref(false)
|
||||
const deleteConfirmText = ref('')
|
||||
const deletePassword = ref('')
|
||||
const deleting = ref(false)
|
||||
|
||||
const pwdForm = reactive({ old_password: '', new_password: '', confirm: '' })
|
||||
const emailForm = reactive({ new_email: '', password: '' })
|
||||
@@ -121,6 +166,20 @@ function handleLogout() {
|
||||
auth.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
deleting.value = true
|
||||
try {
|
||||
await api.delete('/users/me', { data: { password: deletePassword.value } })
|
||||
message.success('账号已注销,感谢你曾来过青叶 🌿')
|
||||
auth.logout()
|
||||
router.push('/login')
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data?.detail || '注销失败,请检查密码')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -147,4 +206,34 @@ function handleLogout() {
|
||||
}
|
||||
.current-email .label { font-size: 13px; color: var(--color-text-secondary); }
|
||||
.current-email .value { font-size: 14px; font-weight: 500; }
|
||||
|
||||
/* 注销账号 */
|
||||
.danger-card { border-color: #FFCDD2; }
|
||||
.danger-title { color: #C62828; }
|
||||
.danger-desc { font-size: 13px; color: var(--color-text-secondary); margin: 0 0 12px; line-height: 1.6; }
|
||||
.danger-desc b { color: #C62828; }
|
||||
|
||||
/* 注销弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px;
|
||||
}
|
||||
.delete-modal {
|
||||
width: 420px; max-width: 100%; background: var(--color-surface); border-radius: 16px; overflow: hidden;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 18px 24px; border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.modal-header h3 { margin: 0; font-size: 17px; }
|
||||
.close-btn { cursor: pointer; font-size: 18px; color: var(--color-text-hint); }
|
||||
.modal-body { padding: 20px 24px; }
|
||||
.warn-box {
|
||||
background: #FFF3E0; border-radius: 8px; padding: 12px; font-size: 13px;
|
||||
color: #E65100; line-height: 1.6; margin-bottom: 16px;
|
||||
}
|
||||
.warn-box ul { margin: 6px 0 0 18px; padding: 0; }
|
||||
.warn-box b { color: #C62828; }
|
||||
.modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 24px; border-top: 1px solid var(--color-border); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user