新增 DyQrCodeService,调用 sso.douyin.com 二维码接口; ConfigController 新增 qrcode/qrstatus/qrbind 三个接口; 前端账号列表每行增加"扫码登录"按钮,展示二维码弹窗并自动轮询绑定。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { FormInstance } from 'ant-design-vue';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { EditFilled } from '@ant-design/icons-vue';
|
||||
import { EditFilled, QrcodeOutlined } from '@ant-design/icons-vue';
|
||||
import { useApiStore } from '@/store';
|
||||
import type { UnwrapRef } from 'vue';
|
||||
|
||||
@@ -220,6 +220,77 @@ const removeRow = (index) => {
|
||||
form.upSecUserIdsJson.splice(index, 1);
|
||||
};
|
||||
const rowCount = 4;
|
||||
|
||||
// 扫码登录
|
||||
const showQrModal = ref(false);
|
||||
const qrToken = ref('');
|
||||
const qrBase64 = ref('');
|
||||
const qrStatus = ref(0); // 0=加载中, 1=待扫码, 2=已扫码待确认, 3=登录成功, -1=已过期
|
||||
const qrAccountId = ref('');
|
||||
const qrAccountName = ref('');
|
||||
const qrBindSuccess = ref(false);
|
||||
let qrPollingTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const openQrModal = async (record: DataItem) => {
|
||||
qrAccountId.value = record.id;
|
||||
qrAccountName.value = record.userName;
|
||||
qrStatus.value = 0;
|
||||
qrBase64.value = '';
|
||||
qrToken.value = '';
|
||||
qrBindSuccess.value = false;
|
||||
showQrModal.value = true;
|
||||
try {
|
||||
const res = await useApiStore().QrCodeGet();
|
||||
if (res.code === 0) {
|
||||
qrToken.value = res.data.token;
|
||||
qrBase64.value = res.data.qrBase64;
|
||||
qrStatus.value = 1;
|
||||
startQrPolling();
|
||||
} else {
|
||||
qrStatus.value = -1;
|
||||
}
|
||||
} catch {
|
||||
qrStatus.value = -1;
|
||||
}
|
||||
};
|
||||
|
||||
const startQrPolling = () => {
|
||||
stopQrPolling();
|
||||
qrPollingTimer = setInterval(async () => {
|
||||
if (!qrToken.value) return;
|
||||
try {
|
||||
const res = await useApiStore().QrCodeStatus(qrToken.value);
|
||||
if (res.code === 0) {
|
||||
qrStatus.value = res.data.status;
|
||||
if (res.data.status === 3) {
|
||||
stopQrPolling();
|
||||
const bindRes = await useApiStore().QrCodeBind(qrToken.value, qrAccountId.value);
|
||||
if (bindRes.code === 0) {
|
||||
qrBindSuccess.value = true;
|
||||
setTimeout(() => {
|
||||
showQrModal.value = false;
|
||||
GetRecords();
|
||||
}, 1500);
|
||||
}
|
||||
} else if (res.data.status === -1) {
|
||||
stopQrPolling();
|
||||
}
|
||||
}
|
||||
} catch { /* 忽略轮询网络错误 */ }
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const stopQrPolling = () => {
|
||||
if (qrPollingTimer) {
|
||||
clearInterval(qrPollingTimer);
|
||||
qrPollingTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const closeQrModal = () => {
|
||||
stopQrPolling();
|
||||
showQrModal.value = false;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a-modal :title="form._isNew ? '新增Cookie' : '编辑Cookie'" v-model:visible="showModal" @ok="submit" @cancel="cancel" width="1000px">
|
||||
@@ -374,6 +445,12 @@ const rowCount = 4;
|
||||
</template>
|
||||
删除
|
||||
</a-button>
|
||||
<a-button type="link" @click="openQrModal(record)" size="small">
|
||||
<template #icon>
|
||||
<QrcodeOutlined />
|
||||
</template>
|
||||
扫码登录
|
||||
</a-button>
|
||||
</template>
|
||||
<div v-else class="text-subtext">
|
||||
{{ text }}
|
||||
@@ -381,6 +458,28 @@ const rowCount = 4;
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<a-modal title="抖音扫码登录" :visible="showQrModal" :footer="null" @cancel="closeQrModal" width="360px">
|
||||
<div style="text-align: center; padding: 16px 0;">
|
||||
<div style="margin-bottom: 12px; color: #666;">账号:{{ qrAccountName }}</div>
|
||||
<div v-if="qrStatus === 0" style="padding: 60px 0; color: #999;">加载中...</div>
|
||||
<div v-else-if="qrStatus === -1" style="padding: 40px 0;">
|
||||
<div style="color: #ff4d4f; margin-bottom: 12px;">二维码已过期</div>
|
||||
<a-button @click="openQrModal({ id: qrAccountId, userName: qrAccountName })">刷新二维码</a-button>
|
||||
</div>
|
||||
<div v-else-if="qrBindSuccess" style="padding: 40px 0; color: #52c41a; font-size: 16px;">
|
||||
登录成功,Cookie 已更新!
|
||||
</div>
|
||||
<div v-else>
|
||||
<img v-if="qrBase64" :src="'data:image/png;base64,' + qrBase64" style="width: 200px; height: 200px;" />
|
||||
<div style="margin-top: 12px; font-size: 13px; color: #666;">
|
||||
<span v-if="qrStatus === 1">请用抖音 App 扫描二维码</span>
|
||||
<span v-else-if="qrStatus === 2" style="color: #1890ff;">已扫码,请在手机上确认登录</span>
|
||||
<span v-else-if="qrStatus === 3" style="color: #52c41a;">登录成功,正在保存...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal title="Cookie 详情" :visible="showCookiesModal" style="width:1200px;" @cancel="showCookiesModal = false" @ok="showCookiesModal = false">
|
||||
<div class="cookie-content">
|
||||
{{ showCookiesData || '无Cookie数据' }}
|
||||
|
||||
@@ -200,6 +200,27 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取抖音扫码登录二维码
|
||||
async function QrCodeGet() {
|
||||
return http.request<any, Response<any>>('/api/config/qrcode', 'get').then(r => {
|
||||
return r.data;
|
||||
});
|
||||
}
|
||||
|
||||
// 轮询扫码状态
|
||||
async function QrCodeStatus(token: string) {
|
||||
return http.request<any, Response<any>>('/api/config/qrstatus?token=' + token, 'get').then(r => {
|
||||
return r.data;
|
||||
});
|
||||
}
|
||||
|
||||
// 扫码成功后绑定到账号
|
||||
async function QrCodeBind(token: string, id: string) {
|
||||
return http.request<any, Response<any>>(`/api/config/qrbind?token=${token}&id=${id}`, 'post_json').then(r => {
|
||||
return r.data;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
deleteCookie,
|
||||
UpdateConfig,
|
||||
@@ -216,6 +237,9 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
VideoCategories,
|
||||
VideoCleanup,
|
||||
CookiePageList,
|
||||
DeleteAuthorVideos
|
||||
DeleteAuthorVideos,
|
||||
QrCodeGet,
|
||||
QrCodeStatus,
|
||||
QrCodeBind
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user