feat: alert组件

This commit is contained in:
ggyy
2026-04-25 19:41:23 +08:00
parent fe2c258661
commit 7a5214d7ae
6 changed files with 128 additions and 11 deletions

View File

@@ -0,0 +1,58 @@
<template>
<dialog ref="dialogRef" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">{{ state.title }}</h3>
<p class="py-4 text-base-content/80">{{ state.message }}</p>
<div class="modal-action">
<button class="btn" :class="state.danger ? 'btn-error' : 'btn-primary'" @click="resolve(true)">{{ state.confirmText ?? '确认' }}</button>
<button v-if="!state.alertMode" class="btn btn-ghost" @click="resolve(false)">{{ state.cancelText ?? '取消' }}</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button @click="resolve(false)">close</button></form>
</dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
const dialogRef = ref<HTMLDialogElement>();
const state = reactive({
title: "",
message: "",
confirmText: "确认",
cancelText: "取消",
danger: false,
alertMode: false,
});
let _resolve: ((v: boolean) => void) | null = null;
function resolve(value: boolean) {
dialogRef.value?.close();
_resolve?.(value);_resolve = null;
}
function confirm(options: { title: string; message: string; confirmText?: string; cancelText?: string; danger?: boolean }): Promise<boolean> {
state.title = options.title;
state.message = options.message;
state.confirmText = options.confirmText ?? "确认";
state.cancelText = options.cancelText ?? "取消";
state.danger = options.danger ?? false;
state.alertMode = false;
dialogRef.value?.showModal();
return new Promise((res) => { _resolve = res; });
}
function alert(options: { title: string; message: string; confirmText?: string }): Promise<void> {
state.title = options.title;
state.message = options.message;
state.confirmText = options.confirmText ?? "知道了";
state.alertMode = true;
state.danger = false;
dialogRef.value?.showModal();
return new Promise((res) => { _resolve = () => res(); });
}
defineExpose({ confirm, alert });
</script>

View File

@@ -1,5 +1,49 @@
# 公共组件文档
## ConfirmDialog
全局确认/提示弹窗组件,替代原生 `confirm()``alert()`,基于 daisyUI `<dialog>`
### 暴露方法
| 方法 | 参数 | 返回 | 说明 |
|------|------|------|------|
| `confirm(options)` | 见下表 | `Promise<boolean>` | 确认弹窗,有确认+取消按钮 |
| `alert(options)` | `title`, `message`, `confirmText?` | `Promise<void>` | 提示弹窗,只有"知道了"按钮 |
#### confirm options
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `title` | `string` | — | 弹窗标题 |
| `message` | `string` | — | 弹窗内容 |
| `confirmText` | `string` | `"确认"` | 确认按钮文字 |
| `cancelText` | `string` | `"取消"` | 取消按钮文字 |
| `danger` | `boolean` | `false` | 确认按钮显示为红色(危险操作) |
### 基本用法
```components/ConfirmDialog.vue#L1-5
<ConfirmDialog ref="confirmRef" />
```
```components/ConfirmDialog.vue#L1-10
const confirmRef = useTemplateRef<InstanceType<typeof ConfirmDialog>>("confirmRef");
// 确认弹窗(危险操作)
const ok = await confirmRef.value?.confirm({
title: "删除",
message: "确认删除?此操作不可撤销。",
confirmText: "删除",
danger: true,
});
if (!ok) return;
// 提示弹窗
await confirmRef.value?.alert({ title: "提示", message: "请先选择商品" });
```
## StatusTag
状态标签组件,用于展示订单状态、支付状态、发货状态等。

View File

@@ -107,12 +107,15 @@
</div>
</section>
</section>
<!-- 确认弹窗 -->
<ConfirmDialog ref="confirmRef" />
</template>
<script setup lang="ts">
import { reactive, ref, useTemplateRef } from "vue";
import { useData } from "vike-vue/useData";
import { normalizeTelefuncError } from "../../../lib/app-error";
import ConfirmDialog from "../../../components/ConfirmDialog.vue";
import { onCreateCard } from "./createCard.telefunc";
import { onDeleteUnusedCards } from "./deleteUnusedCards.telefunc";
import { onImportCards } from "./importCards.telefunc";
@@ -130,6 +133,7 @@ const cardPage = ref({ items: [...cards], total: cards.length });
const addModalRef = useTemplateRef<HTMLDialogElement>("addModalRef");
const importModalRef = useTemplateRef<HTMLDialogElement>("importModalRef");
const confirmRef = useTemplateRef<InstanceType<typeof ConfirmDialog>>("confirmRef");
const message = ref("");
const errorMessage = ref("");
@@ -231,7 +235,8 @@ async function handleImportCards() {
}
async function handleDeleteCard(id: number) {
if (!confirm(`确认删除卡密 #${id}?此操作不可撤销。`)) return;
const ok = await confirmRef.value?.confirm({ title: "删除卡密", message: `确认删除卡密 #${id}?此操作不可撤销。`, confirmText: "删除", danger: true });
if (!ok) return;
message.value = "";
errorMessage.value = "";
try {
@@ -245,11 +250,12 @@ async function handleDeleteCard(id: number) {
async function handleDeleteUnused() {
if (!filter.productId) {
alert("请先在筛选区选择商品");
await confirmRef.value?.alert({ title: "提示", message: "请先在筛选区选择商品" });
return;
}
const product = products.find(p => String(p.id) === filter.productId);
if (!confirm(`确认清空「${product?.name ?? filter.productId}」所有未售卡密?此操作不可撤销。`)) return;
const ok = await confirmRef.value?.confirm({ title: "清空未售库存", message: `确认清空「${product?.name ?? filter.productId}」所有未售卡密?此操作不可撤销。`, confirmText: "清空", danger: true });
if (!ok) return;
message.value = "";
errorMessage.value = "";
try {

View File

@@ -84,11 +84,13 @@
</div>
</div>
</section>
<ConfirmDialog ref="confirmRef" />
</template>
<script setup lang="ts">
import { normalizeTelefuncError } from "../../../lib/app-error";
import { reactive, ref } from "vue";
import { reactive, ref, useTemplateRef } from "vue";
import ConfirmDialog from "../../../components/ConfirmDialog.vue";
import { useData } from "vike-vue/useData";
import StatusTag from "../../../components/StatusTag.vue";
import { onDeleteCategory } from "./deleteCategory.telefunc";
@@ -101,6 +103,7 @@ const { categories } = useData<Data>();
const categoryList = ref([...categories]);
const saving = ref(false);
const errorMessage = ref("");
const confirmRef = useTemplateRef<InstanceType<typeof ConfirmDialog>>("confirmRef");
const form = reactive({
id: undefined as number | undefined,
@@ -178,7 +181,7 @@ async function handleToggle(category: (typeof categories)[number]) {
}
async function handleDelete(category: (typeof categories)[number]) {
if (!window.confirm(`确认删除分类${category.name}吗?`)) {
if (!await confirmRef.value?.confirm({ title: "删除分类", message: `确认删除分类"${category.name}"吗?`, confirmText: "删除", danger: true })) {
return;
}

View File

@@ -477,13 +477,15 @@
</div>
</section>
</section>
<ConfirmDialog ref="confirmRef" />
</template>
<script setup lang="ts">
import SecretInput from "../../../components/SecretInput.vue";
import StatusTag from "../../../components/StatusTag.vue";
import ConfirmDialog from "../../../components/ConfirmDialog.vue";
import { normalizeTelefuncError } from "../../../lib/app-error";
import { reactive, ref, computed } from "vue";
import { reactive, ref, computed, useTemplateRef } from "vue";
import { useData } from "vike-vue/useData";
import { onSaveEmailConfig, onDeleteEmailConfig, onSaveEmailPushSettings, onActivateEmailProvider, onClearEmailLogs } from "./saveEmailConfig.telefunc";
import { onSaveEmailTemplate } from "./saveEmailTemplate.telefunc";
@@ -526,6 +528,7 @@ type MailboxItem = {
const { configs, templates, logs: initialLogs, metrics, pushSettings: initialPushSettings } = useData<Data>();
const activeTab = ref<"stats" | "config" | "list" | "template">("stats");
const confirmRef = useTemplateRef<InstanceType<typeof ConfirmDialog>>("confirmRef");
// ===================== Mailbox list =====================
const logList = reactive([...initialLogs]);
@@ -710,7 +713,7 @@ async function handleClearLogs() {
logList.splice(0);
showClearConfirm.value = false;
} catch (error) {
alert(normalizeTelefuncError(error, "清除失败"));
await confirmRef.value?.alert({ title: "错误", message: normalizeTelefuncError(error, "清除失败") });
} finally {
clearingLogs.value = false;
}
@@ -827,7 +830,7 @@ async function handleActivate(item: MailboxItem) {
m.isEnabled = m.id === item.id;
}
} catch (error) {
alert(normalizeTelefuncError(error, "激活失败"));
await confirmRef.value?.alert({ title: "错误", message: normalizeTelefuncError(error, "激活失败") });
}
}

View File

@@ -52,11 +52,13 @@
</div>
</div>
</section>
<ConfirmDialog ref="confirmRef" />
</template>
<script setup lang="ts">
import { normalizeTelefuncError } from "../../../lib/app-error";
import { ref } from "vue";
import { ref, useTemplateRef } from "vue";
import ConfirmDialog from "../../../components/ConfirmDialog.vue";
import { useData } from "vike-vue/useData";
import { formatCents } from "../../../lib/utils/money";
import StatusTag from "../../../components/StatusTag.vue";
@@ -65,9 +67,10 @@ import type { Data } from "./+data";
const { products } = useData<Data>();
const productList = ref([...products]);
const confirmRef = useTemplateRef<InstanceType<typeof ConfirmDialog>>("confirmRef");
async function handleDelete(product: (typeof products)[number]) {
if (!window.confirm(`确认删除商品${product.name}吗?`)) {
if (!await confirmRef.value?.confirm({ title: "删除商品", message: `确认删除商品"${product.name}"吗?`, confirmText: "删除", danger: true })) {
return;
}
@@ -75,7 +78,7 @@ async function handleDelete(product: (typeof products)[number]) {
await onDeleteProduct({ id: product.id });
productList.value = productList.value.filter((item) => item.id !== product.id);
} catch (error) {
window.alert(normalizeTelefuncError(error, "删除失败"));
await confirmRef.value?.alert({ title: "错误", message: normalizeTelefuncError(error, "删除失败") });
}
}
</script>