重构:提取useAdminAction/useAdminListQuerySync + 迁移列表视图到useAdminListPage + 修复formatAutoDeliveryReason缺失case + 消除重复格式化函数 + 修复CSS类冲突

This commit is contained in:
yml
2026-05-19 19:34:40 +08:00
parent 563038aa40
commit bbd0991e52
3 changed files with 183 additions and 168 deletions
@@ -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>
@@ -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,
};
}