|
|
@@ -0,0 +1,385 @@
|
|
|
+<template>
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>制单授权(账号级:一个租户可维护多个授权主体)</span>
|
|
|
+ <el-button
|
|
|
+ v-hasPerm="['module_payment:batch:authorize']"
|
|
|
+ type="primary"
|
|
|
+ :loading="applying"
|
|
|
+ @click="openApplyDialog"
|
|
|
+ >
|
|
|
+ 新增授权
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" :data="list" border stripe>
|
|
|
+ <template #empty>
|
|
|
+ <el-empty description="暂无授权主体,点击右上角「新增授权」生成授权链接" />
|
|
|
+ </template>
|
|
|
+ <el-table-column prop="participant_name" label="主体名称" min-width="140">
|
|
|
+ <template #default="{ row }">{{ row.participant_name || "-" }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="participant_id" label="支付宝UID" min-width="160" />
|
|
|
+ <el-table-column label="服务商" min-width="140">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ providerName(row.service_provider_id) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" width="90">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="STATUS_TAG[row.status] || 'info'">
|
|
|
+ {{ STATUS_TEXT[row.status] || row.status }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="agreement_no" label="协议号" min-width="170">
|
|
|
+ <template #default="{ row }">{{ row.agreement_no || "-" }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="授权链接" min-width="120">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button v-if="row.authorize_link" size="small" link type="primary" @click="showLink(row)">
|
|
|
+ 查看
|
|
|
+ </el-button>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="220" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <template v-if="row.status === 'AUTHING'">
|
|
|
+ <el-button
|
|
|
+ v-hasPerm="['module_payment:batch:authorize']"
|
|
|
+ size="small"
|
|
|
+ @click="handleRebind(row)"
|
|
|
+ >
|
|
|
+ 重新生成链接
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-hasPerm="['module_payment:batch:authorize']"
|
|
|
+ size="small"
|
|
|
+ :loading="queryingNo === row.out_biz_no"
|
|
|
+ @click="handleRefresh(row)"
|
|
|
+ >
|
|
|
+ 刷新状态
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ <span v-else-if="row.status === 'AUTHED' || row.status === 'NORMAL'" class="authed-tip">
|
|
|
+ 已生效(可直接制单)
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div class="mt-4 flex justify-end">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pageNo"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :total="total"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="load"
|
|
|
+ @current-change="load"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 新增授权表单 -->
|
|
|
+ <el-dialog v-model="applyVisible" title="新增制单授权" width="520px" :close-on-click-modal="false">
|
|
|
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
|
|
+ <el-form-item label="主体名称" prop="participant_name">
|
|
|
+ <el-input
|
|
|
+ v-model="form.participant_name"
|
|
|
+ placeholder="如:张三(个人)/ 某某公司"
|
|
|
+ style="max-width: 360px"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="支付宝账号" prop="participant_id">
|
|
|
+ <el-input
|
|
|
+ v-model="form.participant_id"
|
|
|
+ placeholder="支付宝用户ID(2088开头)"
|
|
|
+ style="max-width: 360px"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="服务商" prop="service_provider_id">
|
|
|
+ <el-select
|
|
|
+ v-model="form.service_provider_id"
|
|
|
+ placeholder="选择服务商"
|
|
|
+ filterable
|
|
|
+ style="max-width: 360px"
|
|
|
+ >
|
|
|
+ <el-option v-for="p in providerOptions" :key="p.id" :label="p.name" :value="p.id" />
|
|
|
+ </el-select>
|
|
|
+ <div class="form-item-tip">
|
|
|
+ 无法自动获取服务商时手动选择(与新增企业页一致)
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="applyVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="applying" @click="handleApply">生成授权链接</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 授权链接展示(复制 + 二维码 + 刷新状态) -->
|
|
|
+ <el-dialog v-model="linkVisible" title="授权链接(PC 浏览器打开 / 手机支付宝扫码)" width="640px">
|
|
|
+ <div style="display: flex; align-items: flex-start; gap: 16px">
|
|
|
+ <el-input :model-value="currentLink" readonly style="flex: 1">
|
|
|
+ <template #append>
|
|
|
+ <el-button @click="handleCopyLink">复制</el-button>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ <div class="qrcode-wrapper">
|
|
|
+ <canvas ref="qrcodeCanvas" class="qrcode-canvas"></canvas>
|
|
|
+ <div class="form-item-tip">手机支付宝扫码打开</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="currentOutBizNo" style="margin-top: 12px">
|
|
|
+ 授权单号 / 状态:{{ currentOutBizNo }} / {{ currentStatusText }}
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <el-button
|
|
|
+ v-if="currentStatus === 'AUTHING'"
|
|
|
+ :loading="querying"
|
|
|
+ @click="handleQueryStatus"
|
|
|
+ >
|
|
|
+ 刷新状态
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" @click="linkVisible = false">关闭</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </el-card>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, nextTick, onMounted, reactive, ref } from "vue";
|
|
|
+import BatchPayAPI, { type BatchAuthorizeVO } from "@/api/module_payment/batch";
|
|
|
+import ProviderAPI, { type ServiceProviderOption } from "@/api/module_system/service_provider";
|
|
|
+import { ElMessage, ElMessageBox } from "element-plus";
|
|
|
+import type { FormInstance, FormRules } from "element-plus";
|
|
|
+import QRCode from "qrcode";
|
|
|
+
|
|
|
+const list = ref<BatchAuthorizeVO[]>([]);
|
|
|
+const total = ref(0);
|
|
|
+const pageNo = ref(1);
|
|
|
+const pageSize = ref(20);
|
|
|
+const loading = ref(false);
|
|
|
+const applying = ref(false);
|
|
|
+const querying = ref(false);
|
|
|
+const queryingNo = ref("");
|
|
|
+
|
|
|
+const providerOptions = ref<ServiceProviderOption[]>([]);
|
|
|
+const providerName = (id?: number) =>
|
|
|
+ (providerOptions.value.find((p) => p.id === id)?.name) || "-";
|
|
|
+
|
|
|
+const STATUS_TAG: Record<string, "primary" | "success" | "warning" | "info" | "danger"> = {
|
|
|
+ AUTHING: "warning",
|
|
|
+ AUTHED: "success",
|
|
|
+ NORMAL: "success",
|
|
|
+ UNBIND: "info",
|
|
|
+};
|
|
|
+const STATUS_TEXT: Record<string, string> = {
|
|
|
+ AUTHING: "授权中",
|
|
|
+ AUTHED: "已授权",
|
|
|
+ // 存量兼容: 通知归一前落库的 NORMAL(支付宝生效状态)视同已授权
|
|
|
+ NORMAL: "已授权",
|
|
|
+ UNBIND: "已解绑",
|
|
|
+};
|
|
|
+
|
|
|
+async function load() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await BatchPayAPI.authorizeList({ page_no: pageNo.value, page_size: pageSize.value });
|
|
|
+ // 双保险解包: 后端 PageResult 序列化字段是 list(全局类型声明 items 与实际不符,与存量一致)
|
|
|
+ list.value = res.data.data?.items || res.data.data?.list || [];
|
|
|
+ total.value = res.data.data?.total || 0;
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ load();
|
|
|
+ try {
|
|
|
+ const res = await ProviderAPI.options();
|
|
|
+ providerOptions.value = res.data.data || [];
|
|
|
+ } catch { /* 服务商下拉加载失败不阻塞列表 */ }
|
|
|
+});
|
|
|
+
|
|
|
+// ==================== 新增授权 ====================
|
|
|
+
|
|
|
+const applyVisible = ref(false);
|
|
|
+const formRef = ref<FormInstance>();
|
|
|
+const form = reactive({
|
|
|
+ participant_name: "",
|
|
|
+ participant_id: "",
|
|
|
+ service_provider_id: null as number | null,
|
|
|
+});
|
|
|
+const rules: FormRules = {
|
|
|
+ participant_name: [{ required: true, message: "请输入主体名称", trigger: "blur" }],
|
|
|
+ participant_id: [{ required: true, message: "请输入支付宝账号", trigger: "blur" }],
|
|
|
+ service_provider_id: [{ required: true, message: "请选择服务商", trigger: "change" }],
|
|
|
+};
|
|
|
+
|
|
|
+function openApplyDialog() {
|
|
|
+ formRef.value?.resetFields();
|
|
|
+ applyVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+async function handleApply() {
|
|
|
+ const valid = await formRef.value?.validate().catch(() => false);
|
|
|
+ if (!valid) return;
|
|
|
+ applying.value = true;
|
|
|
+ try {
|
|
|
+ const res = await BatchPayAPI.authorizeApply({
|
|
|
+ participant_name: form.participant_name.trim(),
|
|
|
+ participant_id: form.participant_id.trim(),
|
|
|
+ service_provider_id: form.service_provider_id!,
|
|
|
+ });
|
|
|
+ applyVisible.value = false;
|
|
|
+ // 提交成功 → 展示授权链接弹层
|
|
|
+ currentLink.value = res.data.data.authorize_link;
|
|
|
+ currentOutBizNo.value = res.data.data.out_biz_no;
|
|
|
+ currentStatus.value = res.data.data.status;
|
|
|
+ linkVisible.value = true;
|
|
|
+ nextTick(() => drawQRCode());
|
|
|
+ ElMessage.success("授权链接已生成,请尽快完成授权");
|
|
|
+ load();
|
|
|
+ } finally {
|
|
|
+ applying.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 授权链接展示 ====================
|
|
|
+
|
|
|
+const linkVisible = ref(false);
|
|
|
+const currentLink = ref("");
|
|
|
+const currentOutBizNo = ref("");
|
|
|
+const currentStatus = ref("");
|
|
|
+const qrcodeCanvas = ref<HTMLCanvasElement>();
|
|
|
+
|
|
|
+const currentStatusText = computed(() => STATUS_TEXT[currentStatus.value] || currentStatus.value || "-");
|
|
|
+
|
|
|
+function showLink(row: BatchAuthorizeVO) {
|
|
|
+ currentLink.value = row.authorize_link || "";
|
|
|
+ currentOutBizNo.value = row.out_biz_no;
|
|
|
+ currentStatus.value = row.status;
|
|
|
+ linkVisible.value = true;
|
|
|
+ nextTick(() => drawQRCode());
|
|
|
+}
|
|
|
+
|
|
|
+/** 授权链接二维码(复用原 BatchPayAuthorize 的 qrcode 库用法) */
|
|
|
+async function drawQRCode() {
|
|
|
+ if (!qrcodeCanvas.value || !currentLink.value) return;
|
|
|
+ try {
|
|
|
+ await QRCode.toCanvas(qrcodeCanvas.value, currentLink.value, { width: 160, margin: 1 });
|
|
|
+ } catch (err) {
|
|
|
+ console.error("授权链接二维码生成失败:", err);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleCopyLink() {
|
|
|
+ try {
|
|
|
+ await navigator.clipboard.writeText(currentLink.value);
|
|
|
+ ElMessage.success("授权链接已复制");
|
|
|
+ } catch {
|
|
|
+ ElMessage.warning("复制失败,请手动复制");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 链接弹层内的状态刷新:AUTHED 后回写协议号并提示完成 */
|
|
|
+async function handleQueryStatus() {
|
|
|
+ if (!currentOutBizNo.value) return;
|
|
|
+ querying.value = true;
|
|
|
+ try {
|
|
|
+ const res = await BatchPayAPI.queryAuthorize(currentOutBizNo.value);
|
|
|
+ currentStatus.value = res.data.data.status;
|
|
|
+ if (res.data.data.status === "AUTHED" || res.data.data.status === "NORMAL") {
|
|
|
+ ElMessage.success("授权已完成,可直接制单");
|
|
|
+ linkVisible.value = false;
|
|
|
+ }
|
|
|
+ load();
|
|
|
+ } finally {
|
|
|
+ querying.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 行操作 ====================
|
|
|
+
|
|
|
+/** AUTHING 行: 重新生成(作废旧链接换新 out_biz_no) */
|
|
|
+async function handleRebind(row: BatchAuthorizeVO) {
|
|
|
+ if (row.id == null) {
|
|
|
+ ElMessage.warning("记录缺少 id,无法重新生成");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ "重新生成将作废当前授权链接并换新单号,是否继续?",
|
|
|
+ "重新生成授权链接",
|
|
|
+ { type: "warning", confirmButtonText: "重新生成", cancelButtonText: "取消" }
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ applying.value = true;
|
|
|
+ try {
|
|
|
+ const res = await BatchPayAPI.authorizeRebind(row.id);
|
|
|
+ currentLink.value = res.data.data.authorize_link;
|
|
|
+ currentOutBizNo.value = res.data.data.out_biz_no;
|
|
|
+ currentStatus.value = res.data.data.status;
|
|
|
+ linkVisible.value = true;
|
|
|
+ nextTick(() => drawQRCode());
|
|
|
+ ElMessage.success("已重新生成授权链接");
|
|
|
+ load();
|
|
|
+ } finally {
|
|
|
+ applying.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** AUTHING 行: 刷新状态(AUTHED 后协议号回写本地) */
|
|
|
+async function handleRefresh(row: BatchAuthorizeVO) {
|
|
|
+ queryingNo.value = row.out_biz_no;
|
|
|
+ try {
|
|
|
+ const res = await BatchPayAPI.queryAuthorize(row.out_biz_no);
|
|
|
+ if (res.data.data.status === "AUTHED" || res.data.data.status === "NORMAL") {
|
|
|
+ ElMessage.success("授权已完成,可直接制单");
|
|
|
+ } else {
|
|
|
+ ElMessage.info(`当前状态:${STATUS_TEXT[res.data.data.status] || res.data.data.status}`);
|
|
|
+ }
|
|
|
+ load();
|
|
|
+ } finally {
|
|
|
+ queryingNo.value = "";
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.authed-tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.qrcode-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.qrcode-canvas {
|
|
|
+ width: 160px;
|
|
|
+ height: 160px;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-item-tip {
|
|
|
+ width: 100%;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+</style>
|