|
@@ -53,10 +53,13 @@
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
<el-table-column prop="created_time" label="创建时间" width="180" />
|
|
<el-table-column prop="created_time" label="创建时间" width="180" />
|
|
|
- <el-table-column label="操作" width="140" fixed="right">
|
|
|
|
|
|
|
+ <!-- ★ 「收款链接」对**两种身份都显示**:后端那个端点挂的是权限串(复用本页 list),
|
|
|
|
|
+ 而且两道数据闸只允许取到自己名下的那一行 —— 租户给自己取链接正是它的主用途。
|
|
|
|
|
+ 这与旁边两个按钮(只认超管身份)是两种门禁,别照着其中一个去改另一个,
|
|
|
|
|
+ 见后端 `LeshouyingMerchantController` 的类 javadoc。 -->
|
|
|
|
|
+ <el-table-column label="操作" width="210" fixed="right">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- <!-- 两个按钮都挂 isSuperAdmin:后端 save/delete 已经只认超管身份,
|
|
|
|
|
- 非超管点下去必 403 —— 显示一个点了必然失败的入口比不显示更糟。 -->
|
|
|
|
|
|
|
+ <el-button link type="primary" @click="openLink(row)">收款链接</el-button>
|
|
|
<el-button v-if="isSuperAdmin" link type="primary" @click="openForm(row)">
|
|
<el-button v-if="isSuperAdmin" link type="primary" @click="openForm(row)">
|
|
|
编辑
|
|
编辑
|
|
|
</el-button>
|
|
</el-button>
|
|
@@ -126,6 +129,38 @@
|
|
|
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
|
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
|
|
</template>
|
|
</template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 收款链接(复制 / 二维码 / 下载)—— 一切以「拿走直接用」为准:
|
|
|
|
|
+ 操作员拿到它是要去打印桌牌、贴到收银台、发给顾客的,所以三件都要在场。 -->
|
|
|
|
|
+ <el-dialog v-model="linkVisible" title="收款链接" width="620px">
|
|
|
|
|
+ <el-alert
|
|
|
|
|
+ type="success"
|
|
|
|
|
+ :closable="false"
|
|
|
|
|
+ style="margin-bottom: 16px"
|
|
|
|
|
+ title="这条链接长期有效、可以无限次使用。把它打印成桌牌、贴到收银台,或直接用微信/支付宝扫码——顾客打开后自己输入金额即可付款。"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-form label-width="90px">
|
|
|
|
|
+ <el-form-item label="商户">
|
|
|
|
|
+ <span>{{ link.mch_no }}</span>
|
|
|
|
|
+ <span v-if="link.merchant_name" class="link-name">({{ link.merchant_name }})</span>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="链接">
|
|
|
|
|
+ <div class="link-row">
|
|
|
|
|
+ <el-input :model-value="link.url" readonly />
|
|
|
|
|
+ <div class="qrcode-wrapper">
|
|
|
|
|
+ <!-- ★ 尺寸给到 240:这一块是要**被扫**的,160 在小屏手机上难扫;
|
|
|
|
|
+ 下载出来的 PNG 与屏幕上这块是同一个函数画的,不会两副样子。 -->
|
|
|
|
|
+ <canvas ref="qrcodeCanvas" class="qrcode-canvas"></canvas>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button :disabled="!link.url" @click="handleCopyLink">复制链接</el-button>
|
|
|
|
|
+ <el-button :disabled="!link.url" @click="handleDownloadQr">下载二维码</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="linkVisible = false">关闭</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -140,12 +175,14 @@ defineOptions({
|
|
|
inheritAttrs: false,
|
|
inheritAttrs: false,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-import { computed, onMounted, reactive, ref } from "vue";
|
|
|
|
|
-import { ElMessageBox, type FormRules } from "element-plus";
|
|
|
|
|
|
|
+import { computed, nextTick, onMounted, reactive, ref } from "vue";
|
|
|
|
|
+import { ElMessage, ElMessageBox, type FormRules } from "element-plus";
|
|
|
|
|
+import QRCode from "qrcode";
|
|
|
import {
|
|
import {
|
|
|
getLsyMerchantPage,
|
|
getLsyMerchantPage,
|
|
|
saveLsyMerchant,
|
|
saveLsyMerchant,
|
|
|
deleteLsyMerchant,
|
|
deleteLsyMerchant,
|
|
|
|
|
+ getLsyCheckoutLink,
|
|
|
} from "@/api/module_payment/collection";
|
|
} from "@/api/module_payment/collection";
|
|
|
import TenantAPI, { type TenantTable } from "@/api/module_system/tenant";
|
|
import TenantAPI, { type TenantTable } from "@/api/module_system/tenant";
|
|
|
import { useUserStore } from "@/store";
|
|
import { useUserStore } from "@/store";
|
|
@@ -318,6 +355,92 @@ async function handleSubmit() {
|
|
|
loadData();
|
|
loadData();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ────────────────────────── 收款链接 ──────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 链接弹窗的三格。**只认这三格**,与后端 `LsyCheckoutLinkVO` 逐字段对应
|
|
|
|
|
+ * (其余一概不取:租户号、门店号、主键、token 都不回给页面 —— 见那个 VO 的 javadoc)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ★ `merchant_name` 是**链接行上的快照**,不是表格里那一列:商户改过名但还没重新取过链接时
|
|
|
|
|
+ * 两者会不同,而操作员看到的名字必须与顾客打开链接时看到的一样。
|
|
|
|
|
+ * ★ `url` 整条来自后端 `buildUrl`。**不要**在这里用 `mch_no` 或域名自己拼:
|
|
|
|
|
+ * 基址推导只有那一份,拼第二个副本的后果是「复制的链接」与「二维码里的链接」不是同一条。
|
|
|
|
|
+ */
|
|
|
|
|
+const link = ref<{ mch_no?: string; merchant_name?: string | null; url?: string }>({});
|
|
|
|
|
+const linkVisible = ref(false);
|
|
|
|
|
+const qrcodeCanvas = ref<HTMLCanvasElement>();
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 打开链接弹窗:**先取数据、拿到之后再开窗**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ★ 顺序是有意的:这个端点是 POST 且会**写一行**(首次调用建链接),
|
|
|
|
|
+ * 先开窗会让操作员盯着一个空框等一次网络往返,失败时更是一个空弹窗配一句 toast。
|
|
|
|
|
+ * 取不到就压根不弹 —— 列表页上的 toast 已经把原因说清楚了。
|
|
|
|
|
+ */
|
|
|
|
|
+async function openLink(row: any) {
|
|
|
|
|
+ link.value = {};
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res: any = await getLsyCheckoutLink(row.id);
|
|
|
|
|
+ // 两层 data:响应拦截器 `return response`,**不解包**(同 loadData 的实测)。
|
|
|
|
|
+ link.value = res.data.data || {};
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 拦截器已经用服务端的原话弹过 toast(404 商户不存在 / 403 无归属租户)。
|
|
|
|
|
+ // 这里什么都不做:留在列表页,操作员可以直接再点一次。
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ linkVisible.value = true;
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ await drawQr();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 画二维码。★ 传**整条 url**(不是 token)—— 顾客扫出来的必须就是那条链接。 */
|
|
|
|
|
+async function drawQr() {
|
|
|
|
|
+ if (!qrcodeCanvas.value || !link.value.url) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ await QRCode.toCanvas(qrcodeCanvas.value, link.value.url, { width: 240, margin: 1 });
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 画不出来 = 这一屏少了「扫码」这条腿,必须说出来,不能只留在控制台。
|
|
|
|
|
+ // 链接本身还在输入框里,复制那条腿不受影响。
|
|
|
|
|
+ ElMessage.error("二维码生成失败,请改用「复制链接」");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleCopyLink() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navigator.clipboard.writeText(link.value.url || "");
|
|
|
|
|
+ ElMessage.success("收款链接已复制");
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 非 https / 无剪贴板权限时 `navigator.clipboard` 不可用(或写入被拒),
|
|
|
|
|
+ // 输入框本身是可全选的,所以这里给一句能照做的提示。
|
|
|
|
|
+ ElMessage.warning("复制失败,请手动选中链接复制");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 下载二维码 PNG。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ★ 从**同一份数据**(`link.value.url`)重画一张,而不是把屏幕上那块 canvas 截图:
|
|
|
|
|
+ * 屏幕上的 canvas 受 `devicePixelRatio` 与 CSS 尺寸影响,截出来的图在打印场景里
|
|
|
|
|
+ * 会忽大忽小;`toDataURL` 给的是一张尺寸确定的位图,桌牌打印要的正是这个。
|
|
|
|
|
+ */
|
|
|
|
|
+async function handleDownloadQr() {
|
|
|
|
|
+ if (!link.value.url) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const dataUrl = await QRCode.toDataURL(link.value.url, { width: 600, margin: 2 });
|
|
|
|
|
+ const a = document.createElement("a");
|
|
|
|
|
+ a.href = dataUrl;
|
|
|
|
|
+ // 文件名带上商户号:一次导几张时不会互相覆盖,也不会分不清哪张是哪家。
|
|
|
|
|
+ a.download = `收款码_${link.value.mch_no || "商户"}.png`;
|
|
|
|
|
+ a.click();
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ ElMessage.error("二维码下载失败,请改用「复制链接」");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function handleDelete(row: any) {
|
|
async function handleDelete(row: any) {
|
|
|
// ★ 2026-09-30 去企业化:这里原先是 `row.enterprise_id ? \`企业「…」\` : "本租户"`。
|
|
// ★ 2026-09-30 去企业化:这里原先是 `row.enterprise_id ? \`企业「…」\` : "本租户"`。
|
|
|
// 企业那一维度没了,措辞改成被删的那一行自己的两项身份(商户号 + 归属租户),
|
|
// 企业那一维度没了,措辞改成被删的那一行自己的两项身份(商户号 + 归属租户),
|
|
@@ -366,4 +489,38 @@ onMounted(async () => {
|
|
|
color: var(--el-text-color-placeholder);
|
|
color: var(--el-text-color-placeholder);
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 链接 + 二维码并排:输入框可伸缩,二维码固定 240(见模板里那段尺寸说明) */
|
|
|
|
|
+.link-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.link-row :deep(.el-input) {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.qrcode-wrapper {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ padding: 4px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ★ 显式与 `drawQr` 里那个 `width: 240` 同值:canvas 的**显示尺寸**由这两行决定,
|
|
|
|
|
+ 而它的位图尺寸由那个参数决定。两者不一致时二维码看着正常、扫起来却可能因缩放发虚。 */
|
|
|
|
|
+.qrcode-canvas {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 240px;
|
|
|
|
|
+ height: 240px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.link-name {
|
|
|
|
|
+ margin-left: 4px;
|
|
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|