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