重构:提取useAdminAction/useAdminListQuerySync + 迁移列表视图到useAdminListPage + 修复formatAutoDeliveryReason缺失case + 消除重复格式化函数 + 修复CSS类冲突
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -309,10 +309,18 @@ function resetFilters() {
|
||||
>
|
||||
<span
|
||||
class="cell-subline"
|
||||
:title="formatBindingRoleSummary(row.bindingSummary.roleKeys)"
|
||||
:title="
|
||||
formatBindingRoleSummary(
|
||||
row.bindingSummary.roleKeys,
|
||||
'无绑定角色'
|
||||
)
|
||||
"
|
||||
>绑定
|
||||
{{
|
||||
formatBindingRoleSummary(row.bindingSummary.roleKeys)
|
||||
formatBindingRoleSummary(
|
||||
row.bindingSummary.roleKeys,
|
||||
"无绑定角色"
|
||||
)
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
@@ -320,7 +328,6 @@ function resetFilters() {
|
||||
:title="formatBindingCountSummary(row.bindingSummary)"
|
||||
>{{ formatBindingCountSummary(row.bindingSummary) }}</span
|
||||
>
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
+168
-162
@@ -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<ClaimDetailData | null>(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<ClaimDetailData | null>(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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user