diff --git a/apps/frontend/src/styles/admin-config-pages.css b/apps/frontend/src/styles/admin-config-pages.css
index 6b2bc7f0..a582ab35 100644
--- a/apps/frontend/src/styles/admin-config-pages.css
+++ b/apps/frontend/src/styles/admin-config-pages.css
@@ -147,12 +147,12 @@
}
/* ---------- 数据表 (用于 el-table,具体单元格样式见各页面 :deep() 规则) ---------- */
-.data-table {
+.data-table--config {
width: 100%;
margin-top: 12px;
}
-.cell-stack {
+.cell-stack--config {
display: grid;
gap: 4px;
}
@@ -233,4 +233,6 @@
margin: 10px 0 0;
font-size: var(--text-xs);
}
-.mt-4 { margin-top: 16px; }
+.mt-4 {
+ margin-top: 16px;
+}
diff --git a/apps/frontend/src/views/admin/tasks/AdminTasksView.vue b/apps/frontend/src/views/admin/tasks/AdminTasksView.vue
index 93df3b12..e4de8c53 100644
--- a/apps/frontend/src/views/admin/tasks/AdminTasksView.vue
+++ b/apps/frontend/src/views/admin/tasks/AdminTasksView.vue
@@ -309,10 +309,18 @@ function resetFilters() {
>
绑定
{{
- formatBindingRoleSummary(row.bindingSummary.roleKeys)
+ formatBindingRoleSummary(
+ row.bindingSummary.roleKeys,
+ "无绑定角色"
+ )
}}
{{ formatBindingCountSummary(row.bindingSummary) }}
- >
diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts b/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts
index 869abf69..90decbe7 100644
--- a/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts
+++ b/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts
@@ -1,325 +1,331 @@
-import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
-import QRCode from 'qrcode'
-import { ElMessageBox } from 'element-plus'
+import { computed, onBeforeUnmount, onMounted, ref } from "vue";
+import QRCode from "qrcode";
+import { ElMessageBox } from "element-plus";
-import { showError, showSuccess } from '@/lib/feedback'
+import { showError, showSuccess } from "@/lib/feedback";
+import { formatAdminDateTime } from "@/utils/admin-time";
import {
confirmKuaishouCloudClaimRole,
fetchClaimDetail,
redeemKuaishouCloudClaim,
verifyKuaishouCloudClaimTicket,
-} from '@/services/claim'
-import type { ClaimDetailData } from '@/types/claim'
+} from "@/services/claim";
+import type { ClaimDetailData } from "@/types/claim";
export function useKuaishouCloudClaim(token: () => string) {
- const loading = ref(true)
- const submitting = ref(false)
- const refreshingRole = ref(false)
- const confirmingRole = ref(false)
- const redeeming = ref(false)
- const errorMessage = ref('')
- const detail = ref(null)
- const ticketCode = ref('')
- const qrCodeDataUrl = ref('')
- let pollTimer = 0
+ const loading = ref(true);
+ const submitting = ref(false);
+ const refreshingRole = ref(false);
+ const confirmingRole = ref(false);
+ const redeeming = ref(false);
+ const errorMessage = ref("");
+ const detail = ref(null);
+ const ticketCode = ref("");
+ const qrCodeDataUrl = ref("");
+ let pollTimer = 0;
// ── derived data ────────────────────────────────────────
- const flow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
- const order = computed(() => detail.value?.order || null)
- const orderItem = computed(() => detail.value?.orderItem || null)
- const task = computed(() => detail.value?.task || null)
+ const flow = computed(() => detail.value?.kuaishouCloudFulfillment || null);
+ const order = computed(() => detail.value?.order || null);
+ const orderItem = computed(() => detail.value?.orderItem || null);
+ const task = computed(() => detail.value?.task || null);
// ── computed status flags ───────────────────────────────
- const roleName = computed(() => flow.value?.role.name || flow.value?.binding.roleName || '')
- const roleId = computed(() => flow.value?.role.rid || flow.value?.binding.roleId || '')
- const isTicketVerified = computed(() => flow.value?.ticket.status === 'verified')
+ const roleName = computed(
+ () => flow.value?.role.name || flow.value?.binding.roleName || ""
+ );
+ const roleId = computed(
+ () => flow.value?.role.rid || flow.value?.binding.roleId || ""
+ );
+ const isTicketVerified = computed(
+ () => flow.value?.ticket.status === "verified"
+ );
const isBindUrlExpired = computed(() => {
- const expiresAt = String(flow.value?.binding.bindExpiresAt || '').trim()
+ const expiresAt = String(flow.value?.binding.bindExpiresAt || "").trim();
if (!expiresAt) {
- return false
+ return false;
}
- const expiresTime = Date.parse(expiresAt)
- return Number.isFinite(expiresTime) && expiresTime <= Date.now()
- })
+ const expiresTime = Date.parse(expiresAt);
+ return Number.isFinite(expiresTime) && expiresTime <= Date.now();
+ });
const isBindingPrepared = computed(
() =>
- flow.value?.binding.prepareStatus === 'ready' &&
- Boolean(String(flow.value?.binding.bindUrl || '').trim()) &&
- !isBindUrlExpired.value,
- )
+ flow.value?.binding.prepareStatus === "ready" &&
+ Boolean(String(flow.value?.binding.bindUrl || "").trim()) &&
+ !isBindUrlExpired.value
+ );
- const isBindingPreparing = computed(() => flow.value?.binding.prepareStatus === 'pending')
- const canEnterBindingStep = computed(() => isTicketVerified.value)
- const isRoleReady = computed(() => Boolean(roleName.value || roleId.value))
- const isRoleConfirmed = computed(() => String(task.value?.status || '').trim() === 'role_confirmed')
- const isDispatched = computed(() => String(flow.value?.dispatch.status || '').trim() === 'success')
+ const isBindingPreparing = computed(
+ () => flow.value?.binding.prepareStatus === "pending"
+ );
+ const canEnterBindingStep = computed(() => isTicketVerified.value);
+ const isRoleReady = computed(() => Boolean(roleName.value || roleId.value));
+ const isRoleConfirmed = computed(
+ () => String(task.value?.status || "").trim() === "role_confirmed"
+ );
+ const isDispatched = computed(
+ () => String(flow.value?.dispatch.status || "").trim() === "success"
+ );
const isCompleted = computed(
() =>
- String(task.value?.status || '').trim() === 'completed' ||
- String(flow.value?.consume.status || '').trim() === 'success',
- )
+ String(task.value?.status || "").trim() === "completed" ||
+ String(flow.value?.consume.status || "").trim() === "success"
+ );
const hasRedeemResult = computed(() => {
- const status = String(task.value?.status || '').trim()
+ const status = String(task.value?.status || "").trim();
return (
isDispatched.value ||
- ['dispatched_pending_return', 'completed', 'manual_review', 'failed'].includes(status)
- )
- })
+ [
+ "dispatched_pending_return",
+ "completed",
+ "manual_review",
+ "failed",
+ ].includes(status)
+ );
+ });
const canSubmitTicket = computed(
() =>
- !['completed', 'manual_review', 'closed', 'expired'].includes(
- String(task.value?.status || '').trim(),
- ) && !submitting.value,
- )
+ !["completed", "manual_review", "closed", "expired"].includes(
+ String(task.value?.status || "").trim()
+ ) && !submitting.value
+ );
const currentStep = computed(() => {
if (hasRedeemResult.value) {
- return 4
+ return 4;
}
if (isRoleConfirmed.value) {
- return 3
+ return 3;
}
if (canEnterBindingStep.value) {
- return 2
+ return 2;
}
- return 1
- })
+ return 1;
+ });
const progressText = computed(() => {
if (hasRedeemResult.value) {
- return '兑换结果已生成'
+ return "兑换结果已生成";
}
if (isRoleConfirmed.value) {
- return '角色已确认,等待兑换'
+ return "角色已确认,等待兑换";
}
if (isTicketVerified.value) {
- return isBindingPrepared.value ? '请完成扫码绑定' : '绑定链接刷新中,请稍候'
+ return isBindingPrepared.value
+ ? "请完成扫码绑定"
+ : "绑定链接刷新中,请稍候";
}
- return '等待提交核销码'
- })
+ return "等待提交核销码";
+ });
const resultTitle = computed(() => {
if (isCompleted.value) {
- return '兑换成功'
+ return "兑换成功";
}
if (isDispatched.value) {
- return '兑换请求已提交'
+ return "兑换请求已提交";
}
- return '结果已记录'
- })
+ return "结果已记录";
+ });
const resultDescription = computed(() => {
if (isCompleted.value) {
- return '当前兑换流程已经完成。发货、退号和核销结果会保存在后台任务界面。'
+ return "当前兑换流程已经完成。发货、退号和核销结果会保存在后台任务界面。";
}
- return '你的兑换请求已经提交。发货、退号和核销结果会由系统保存在后台任务界面,无需在此页面等待。'
- })
+ return "你的兑换请求已经提交。发货、退号和核销结果会由系统保存在后台任务界面,无需在此页面等待。";
+ });
// ── actions ─────────────────────────────────────────────
async function generateQRCode(url: string) {
if (!url) {
- qrCodeDataUrl.value = ''
- return
+ qrCodeDataUrl.value = "";
+ return;
}
try {
qrCodeDataUrl.value = await QRCode.toDataURL(url, {
width: 280,
margin: 2,
color: {
- dark: '#0f172a',
- light: '#ffffff',
+ dark: "#0f172a",
+ light: "#ffffff",
},
- })
+ });
} catch (error) {
- qrCodeDataUrl.value = ''
- console.error('生成二维码失败:', error)
+ qrCodeDataUrl.value = "";
+ console.error("生成二维码失败:", error);
}
}
async function applyDetail(nextDetail: ClaimDetailData) {
- detail.value = nextDetail
+ detail.value = nextDetail;
if (!ticketCode.value && nextDetail.kuaishouCloudFulfillment?.ticket.code) {
- ticketCode.value = nextDetail.kuaishouCloudFulfillment.ticket.code
+ ticketCode.value = nextDetail.kuaishouCloudFulfillment.ticket.code;
}
- await generateQRCode(String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || '').trim())
+ await generateQRCode(
+ String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || "").trim()
+ );
}
async function loadDetail(options: { silent?: boolean } = {}) {
if (!options.silent) {
- loading.value = true
+ loading.value = true;
}
- errorMessage.value = ''
+ errorMessage.value = "";
try {
- const response = await fetchClaimDetail(token())
- await applyDetail(response.data)
- syncPolling()
+ const response = await fetchClaimDetail(token());
+ await applyDetail(response.data);
+ syncPolling();
} catch (error) {
- errorMessage.value = error instanceof Error ? error.message : '读取领取信息失败'
- stopPolling()
+ errorMessage.value =
+ error instanceof Error ? error.message : "读取领取信息失败";
+ stopPolling();
} finally {
if (!options.silent) {
- loading.value = false
+ loading.value = false;
}
}
}
async function submitTicket() {
- const normalizedTicketCode = ticketCode.value.trim()
+ const normalizedTicketCode = ticketCode.value.trim();
if (!normalizedTicketCode) {
- showError('请输入核销码')
- return
+ showError("请输入核销码");
+ return;
}
- submitting.value = true
+ submitting.value = true;
try {
const response = await verifyKuaishouCloudClaimTicket(token(), {
ticketCode: normalizedTicketCode,
- })
- await applyDetail(response.data)
- showSuccess('核销码验证通过,系统已开始准备绑定资源')
- syncPolling()
+ });
+ await applyDetail(response.data);
+ showSuccess("核销码验证通过,系统已开始准备绑定资源");
+ syncPolling();
} catch (error) {
- showError(error instanceof Error ? error.message : '核销码验证失败')
+ showError(error instanceof Error ? error.message : "核销码验证失败");
} finally {
- submitting.value = false
+ submitting.value = false;
}
}
async function refreshRole() {
- refreshingRole.value = true
+ refreshingRole.value = true;
try {
- await loadDetail({ silent: true })
+ await loadDetail({ silent: true });
if (isRoleReady.value) {
- showSuccess('角色信息已刷新')
+ showSuccess("角色信息已刷新");
} else {
showError(
- flow.value?.role.errorMessage || '暂时还没有识别到角色信息,请完成绑定后稍等片刻再试',
- )
+ flow.value?.role.errorMessage ||
+ "暂时还没有识别到角色信息,请完成绑定后稍等片刻再试"
+ );
}
} finally {
- refreshingRole.value = false
+ refreshingRole.value = false;
}
}
async function confirmRole() {
- confirmingRole.value = true
+ confirmingRole.value = true;
try {
- const response = await confirmKuaishouCloudClaimRole(token())
- await applyDetail(response.data)
- showSuccess('角色已确认,进入下一步')
- syncPolling()
+ const response = await confirmKuaishouCloudClaimRole(token());
+ await applyDetail(response.data);
+ showSuccess("角色已确认,进入下一步");
+ syncPolling();
} catch (error) {
- showError(error instanceof Error ? error.message : '确认角色失败')
+ showError(error instanceof Error ? error.message : "确认角色失败");
} finally {
- confirmingRole.value = false
+ confirmingRole.value = false;
}
}
async function confirmRedeem() {
try {
await ElMessageBox.confirm(
- '兑换后不可取消,也不可退货。请确认角色和商品信息完全正确。',
- '确认兑换',
+ "兑换后不可取消,也不可退货。请确认角色和商品信息完全正确。",
+ "确认兑换",
{
- confirmButtonText: '确认兑换',
- cancelButtonText: '取消',
- type: 'warning',
+ confirmButtonText: "确认兑换",
+ cancelButtonText: "取消",
+ type: "warning",
center: true,
- },
- )
+ }
+ );
} catch {
- return
+ return;
}
- redeeming.value = true
+ redeeming.value = true;
try {
- const response = await redeemKuaishouCloudClaim(token())
- await applyDetail(response.data)
- showSuccess('兑换请求已提交')
- syncPolling()
+ const response = await redeemKuaishouCloudClaim(token());
+ await applyDetail(response.data);
+ showSuccess("兑换请求已提交");
+ syncPolling();
} catch (error) {
- showError(error instanceof Error ? error.message : '兑换失败')
+ showError(error instanceof Error ? error.message : "兑换失败");
} finally {
- redeeming.value = false
+ redeeming.value = false;
}
}
function openBindUrl(useCurrentPage = false) {
- const bindUrl = String(flow.value?.binding.bindUrl || '').trim()
+ const bindUrl = String(flow.value?.binding.bindUrl || "").trim();
if (!bindUrl) {
- showError('绑定链接还没准备好,请稍后刷新')
- return
+ showError("绑定链接还没准备好,请稍后刷新");
+ return;
}
if (useCurrentPage) {
- window.location.assign(bindUrl)
- return
+ window.location.assign(bindUrl);
+ return;
}
- window.open(bindUrl, '_blank', 'noopener,noreferrer')
+ window.open(bindUrl, "_blank", "noopener,noreferrer");
}
// ── polling ─────────────────────────────────────────────
function stopPolling() {
if (pollTimer) {
- window.clearInterval(pollTimer)
- pollTimer = 0
+ window.clearInterval(pollTimer);
+ pollTimer = 0;
}
}
function syncPolling() {
- const taskStatus = String(task.value?.status || '').trim()
+ const taskStatus = String(task.value?.status || "").trim();
const shouldPoll =
- Boolean(flow.value) && !hasRedeemResult.value && !['closed', 'expired'].includes(taskStatus)
+ Boolean(flow.value) &&
+ !hasRedeemResult.value &&
+ !["closed", "expired"].includes(taskStatus);
if (!shouldPoll) {
- stopPolling()
- return
+ stopPolling();
+ return;
}
if (pollTimer) {
- return
+ return;
}
pollTimer = window.setInterval(() => {
- void loadDetail({ silent: true })
- }, 4000)
- }
-
- // ── utility ─────────────────────────────────────────────
-
- function formatDateTime(value: string | null) {
- if (!value) {
- return '-'
- }
- try {
- return new Date(value).toLocaleString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- })
- } catch {
- return value
- }
+ void loadDetail({ silent: true });
+ }, 4000);
}
// ── lifecycle ───────────────────────────────────────────
onMounted(() => {
- void loadDetail()
- })
+ void loadDetail();
+ });
onBeforeUnmount(() => {
- stopPolling()
- })
+ stopPolling();
+ });
return {
// state
@@ -363,6 +369,6 @@ export function useKuaishouCloudClaim(token: () => string) {
confirmRedeem,
openBindUrl,
// utility
- formatDateTime,
- }
+ formatAdminDateTime,
+ };
}