feat: 更新 '@capp/eden' 依赖至 0.0.10,优化地址管理表单字段和逻辑

This commit is contained in:
2026-01-18 21:06:07 +07:00
parent afc806381e
commit 4cdbb5cac1
4 changed files with 66 additions and 109 deletions

14
pnpm-lock.yaml generated
View File

@@ -52,8 +52,8 @@ catalogs:
specifier: 8.0.0 specifier: 8.0.0
version: 8.0.0 version: 8.0.0
'@capp/eden': '@capp/eden':
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz specifier: http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz
version: 0.0.9 version: 0.0.10
'@cloudflare/workers-types': '@cloudflare/workers-types':
specifier: ^4.20260113.0 specifier: ^4.20260113.0
version: 4.20260116.0 version: 4.20260116.0
@@ -298,7 +298,7 @@ importers:
version: 8.0.0(@capacitor/core@8.0.0) version: 8.0.0(@capacitor/core@8.0.0)
'@capp/eden': '@capp/eden':
specifier: 'catalog:' specifier: 'catalog:'
version: http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))) version: http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))
'@elysiajs/eden': '@elysiajs/eden':
specifier: 'catalog:' specifier: 'catalog:'
version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
@@ -1182,9 +1182,9 @@ packages:
'@capacitor/synapse@1.0.4': '@capacitor/synapse@1.0.4':
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==} resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz': '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz':
resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz} resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz}
version: 0.0.9 version: 0.0.10
peerDependencies: peerDependencies:
'@elysiajs/eden': ^1.4.6 '@elysiajs/eden': ^1.4.6
@@ -6903,7 +6903,7 @@ snapshots:
'@capacitor/synapse@1.0.4': {} '@capacitor/synapse@1.0.4': {}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))': '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))':
dependencies: dependencies:
'@elysiajs/eden': 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) '@elysiajs/eden': 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))

View File

@@ -18,7 +18,7 @@ catalog:
'@capacitor/keyboard': 8.0.0 '@capacitor/keyboard': 8.0.0
'@capacitor/share': ^8.0.0 '@capacitor/share': ^8.0.0
'@capacitor/status-bar': 8.0.0 '@capacitor/status-bar': 8.0.0
'@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz '@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz
'@cloudflare/workers-types': ^4.20260113.0 '@cloudflare/workers-types': ^4.20260113.0
'@elysiajs/eden': ^1.4.6 '@elysiajs/eden': ^1.4.6
'@faker-js/faker': ^10.2.0 '@faker-js/faker': ^10.2.0

View File

@@ -1,15 +1,20 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { Treaty } from "@elysiajs/eden";
import type { TreatyBody } from "@/api/types";
import { toastController } from "@ionic/vue"; import { toastController } from "@ionic/vue";
import { callOutline, locationOutline, personOutline } from "ionicons/icons"; import { callOutline, locationOutline, personOutline } from "ionicons/icons";
import zod from "zod"; import zod from "zod";
import { client, safeClient } from "@/api";
type Address = TreatyBody<typeof client.api.shipping_address.post>;
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const formData = ref({ const formData = ref<Address>({
name: "", recipientName: "",
phone: "", phoneNumber: "",
address: "", detailAddress: "",
isDefault: false, isDefault: false,
}); });
@@ -18,17 +23,16 @@ const isEditMode = computed(() => !!route.query.id);
// 表单验证 Schema // 表单验证 Schema
const AddressSchema = zod.object({ const AddressSchema = zod.object({
name: zod recipientName: zod
.string() .string()
.min(2, "请输入收货人姓名")
.max(20, "姓名长度不能超过20个字符"), .max(20, "姓名长度不能超过20个字符"),
phone: zod phoneNumber: zod
.string() .string()
.min(1, "请输入手机号") .min(1, "请输入手机号")
.regex(/^1[3-9]\d{9}$/, "请输入正确的手机号码"), .regex(/^1[3-9]\d{9}$/, "请输入正确的手机号码"),
address: zod detailAddress: zod
.string() .string()
.min(5, "请输入详细地址") .min(5, "请输入详细地址")
.max(200, "地址长度不能超过200个字符"), .max(200, "地址长度不能超过200个字符"),
@@ -50,9 +54,9 @@ onMounted(() => {
// TODO: 根据 route.query.id 加载地址数据 // TODO: 根据 route.query.id 加载地址数据
// 模拟数据 // 模拟数据
formData.value = { formData.value = {
name: "张三", recipientName: "张三",
phone: "13800138000", phoneNumber: "13800138000",
address: "北京市朝阳区建国路88号SOHO现代城A座1001室", detailAddress: "北京市朝阳区建国路88号SOHO现代城A座1001室",
isDefault: true, isDefault: true,
}; };
} }
@@ -69,21 +73,18 @@ async function handleSubmit() {
isSubmitting.value = true; isSubmitting.value = true;
try { try {
// TODO: 调用添加/编辑地址 API // TODO: 调用添加/编辑地址 API
// if (isEditMode.value) { if (isEditMode.value) {
// const { data } = await safeClient(client.api.address[route.query.id].put(formData.value)); // const { data } = await safeClient();
// } else { }
// const { data } = await safeClient(client.api.address.post(formData.value)); else {
// } await safeClient(client.api.shipping_address.post({
...formData.value,
// 模拟 API 调用 }));
await new Promise(resolve => setTimeout(resolve, 1000)); }
await showToast(isEditMode.value ? "地址修改成功" : "地址添加成功", "success"); await showToast(isEditMode.value ? "地址修改成功" : "地址添加成功", "success");
router.back(); router.back();
} }
catch (error) {
await showToast("操作失败,请重试", "danger");
}
finally { finally {
isSubmitting.value = false; isSubmitting.value = false;
} }
@@ -122,7 +123,7 @@ async function handleSubmit() {
</div> </div>
<ion-item lines="none" class="input-item"> <ion-item lines="none" class="input-item">
<ion-input <ion-input
v-model="formData.name" v-model="formData.recipientName"
type="text" type="text"
placeholder="请输入收货人姓名" placeholder="请输入收货人姓名"
class="custom-input" class="custom-input"
@@ -139,7 +140,7 @@ async function handleSubmit() {
</div> </div>
<ion-item lines="none" class="input-item"> <ion-item lines="none" class="input-item">
<ion-input <ion-input
v-model="formData.phone" v-model="formData.phoneNumber"
type="tel" type="tel"
placeholder="请输入手机号码" placeholder="请输入手机号码"
class="custom-input" class="custom-input"
@@ -156,7 +157,7 @@ async function handleSubmit() {
</div> </div>
<ion-item lines="none" class="input-item textarea-item"> <ion-item lines="none" class="input-item textarea-item">
<ion-textarea <ion-textarea
v-model="formData.address" v-model="formData.detailAddress"
placeholder="请输入详细地址(省市区街道门牌号等)" placeholder="请输入详细地址(省市区街道门牌号等)"
class="custom-textarea" class="custom-textarea"
:rows="4" :rows="4"

View File

@@ -1,35 +1,15 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { Treaty } from "@elysiajs/eden";
import { alertController, toastController } from "@ionic/vue"; import { alertController, toastController } from "@ionic/vue";
import { addOutline, callOutline, checkmarkCircleOutline, createOutline, locationOutline, trashOutline } from "ionicons/icons"; import { addOutline, callOutline, checkmarkCircleOutline, createOutline, locationOutline, trashOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
type Address = Treaty.Data<typeof client.api.shipping_address.get>["data"][number];
const router = useRouter(); const router = useRouter();
const { data: addresses, execute } = await safeClient(() => client.api.shipping_address.get());
interface Address { async function showToast(message: string, color: "success" | "danger" | "warning" | "light" = "light") {
id: number;
name: string;
phone: string;
address: string;
isDefault: boolean;
}
const addresses = ref<Address[]>([
{
id: 1,
name: "张三",
phone: "13800138000",
address: "北京市朝阳区建国路88号SOHO现代城A座1001室",
isDefault: true,
},
{
id: 2,
name: "李四",
phone: "13900139000",
address: "上海市浦东新区世纪大道1号东方明珠广播电视塔",
isDefault: false,
},
]);
async function showToast(message: string, color: "success" | "danger" | "warning" = "success") {
const toast = await toastController.create({ const toast = await toastController.create({
message, message,
duration: 2000, duration: 2000,
@@ -44,31 +24,21 @@ function handleAdd() {
} }
function handleEdit(address: Address) { function handleEdit(address: Address) {
// TODO: 跳转到编辑页面并传递地址ID
router.push(`/address/add?id=${address.id}`); router.push(`/address/add?id=${address.id}`);
} }
async function handleSetDefault(address: Address) { async function handleSetDefault(address: Address) {
if (address.isDefault) { if (address.isDefault) {
return; return;
} }
await safeClient(client.api.shipping_address({ id: address.id }).default.post());
// 取消所有默认地址
addresses.value.forEach((item) => {
item.isDefault = false;
});
// 设置当前地址为默认
address.isDefault = true;
// TODO: 调用 API 更新默认地址
await showToast("已设为默认地址"); await showToast("已设为默认地址");
execute();
} }
async function handleDelete(address: Address) { async function handleDelete(address: Address) {
const alert = await alertController.create({ const alert = await alertController.create({
header: "确认删除", header: "确认删除",
message: `确定要删除"${address.name}"的收货地址吗?`, message: `确定要删除"${address.recipientName}"的收货地址吗?`,
buttons: [ buttons: [
{ {
text: "取消", text: "取消",
@@ -78,22 +48,9 @@ async function handleDelete(address: Address) {
text: "删除", text: "删除",
role: "destructive", role: "destructive",
handler: async () => { handler: async () => {
// 如果删除的是默认地址,需要先设置其他地址为默认 await safeClient(client.api.shipping_address({ id: address.id }).delete());
if (address.isDefault && addresses.value.length > 1) {
const otherAddress = addresses.value.find(item => item.id !== address.id);
if (otherAddress) {
otherAddress.isDefault = true;
}
}
// 删除地址
const index = addresses.value.findIndex(item => item.id === address.id);
if (index > -1) {
addresses.value.splice(index, 1);
}
// TODO: 调用 API 删除地址
await showToast("删除成功"); await showToast("删除成功");
execute();
}, },
}, },
], ],
@@ -120,10 +77,24 @@ async function handleDelete(address: Address) {
</ion-header> </ion-header>
<ion-content> <ion-content>
<div v-if="addresses.length > 0" class="ion-padding"> <div v-if="addresses?.data.length === 0" class="empty-state">
<empty title="暂无收货地址">
<template #icon>
<ion-icon :icon="locationOutline" class="empty-icon" />
</template>
<template #extra>
<ion-button class="add-button" @click="handleAdd">
<ion-icon slot="start" :icon="addOutline" />
添加收货地址
</ion-button>
</template>
</empty>
</div>
<div v-else class="ion-padding">
<div class="space-y-3"> <div class="space-y-3">
<div <div
v-for="address in addresses" v-for="address in addresses?.data"
:key="address.id" :key="address.id"
class="address-card" class="address-card"
> >
@@ -132,8 +103,8 @@ async function handleDelete(address: Address) {
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<ion-icon :icon="locationOutline" class="text-xl text-primary" /> <ion-icon :icon="locationOutline" class="text-xl text-primary" />
<span class="name">{{ address.name }}</span> <span class="name">{{ address.recipientName }}</span>
<span class="phone">{{ address.phone }}</span> <span class="phone">{{ address.phoneNumber }}</span>
</div> </div>
<ion-badge v-if="address.isDefault" color="danger" class="default-badge"> <ion-badge v-if="address.isDefault" color="danger" class="default-badge">
默认 默认
@@ -141,7 +112,7 @@ async function handleDelete(address: Address) {
</div> </div>
<div class="address-text"> <div class="address-text">
{{ address.address }} {{ address.detailAddress }}
</div> </div>
</div> </div>
@@ -150,6 +121,7 @@ async function handleDelete(address: Address) {
<ion-button <ion-button
fill="clear" fill="clear"
size="small" size="small"
:color="address.isDefault ? 'primary' : 'medium'"
@click="handleSetDefault(address)" @click="handleSetDefault(address)"
> >
<ion-icon slot="start" :icon="checkmarkCircleOutline" /> <ion-icon slot="start" :icon="checkmarkCircleOutline" />
@@ -182,23 +154,7 @@ async function handleDelete(address: Address) {
</div> </div>
</div> </div>
<!-- 空状态 --> <div v-if="addresses?.data && addresses.data.length > 0" class="fixed-bottom">
<div v-else class="empty-state">
<empty title="暂无收货地址">
<template #icon>
<ion-icon :icon="locationOutline" class="empty-icon" />
</template>
<template #extra>
<ion-button class="add-button" @click="handleAdd">
<ion-icon slot="start" :icon="addOutline" />
添加收货地址
</ion-button>
</template>
</empty>
</div>
<!-- 底部添加按钮 -->
<div v-if="addresses.length > 0" class="fixed-bottom">
<ion-button expand="block" class="add-button" @click="handleAdd"> <ion-button expand="block" class="add-button" @click="handleAdd">
<ion-icon slot="start" :icon="addOutline" /> <ion-icon slot="start" :icon="addOutline" />
添加新地址 添加新地址