diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 73b221d..9a7c9a2 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -28,5 +28,6 @@ module.exports = {
"no-debugger": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/ban-types": "off",
},
};
diff --git a/src/assets/json/goods1.js b/src/assets/json/goods1.ts
similarity index 100%
rename from src/assets/json/goods1.js
rename to src/assets/json/goods1.ts
diff --git a/src/components/layout/RightDrawer.vue b/src/components/layout/RightDrawer.vue
index 11fa0a2..5c3f7f9 100644
--- a/src/components/layout/RightDrawer.vue
+++ b/src/components/layout/RightDrawer.vue
@@ -1,6 +1,6 @@
-
shopping cart
- 2
+
+ {{ cart.effectiveListCounts }}
contact us
@@ -104,4 +106,9 @@ const toPageCart = () => {
});
open.value = false;
};
+
+import useStore from "@/stores";
+
+const { cart } = useStore();
+cart.getCartList();
diff --git a/src/components/message/index.ts b/src/components/message/index.ts
new file mode 100644
index 0000000..9097003
--- /dev/null
+++ b/src/components/message/index.ts
@@ -0,0 +1,33 @@
+import { AppContext } from "vue";
+import _message from "./src/messageList.vue";
+import MessageManager from "./src/instance";
+import { MessageInstance, MessageItem, MessageType } from "./src/type";
+export const isString = (data: any): boolean => typeof data === "string";
+
+let msg: MessageManager;
+const types = ["text", "success", "warning", "error", "loading"] as const;
+
+const message = types.reduce((pre, value) => {
+ pre[value] = (config: MessageInstance, appContext?: AppContext) => {
+ // 直接传入消息提示内容的情况
+ if (isString(config)) {
+ config = { content: config as string } as MessageItem;
+ }
+
+ const _config: MessageItem = { type: value as MessageType, ...(config as MessageItem) };
+ if (!msg) {
+ msg = new MessageManager(appContext);
+ }
+ return msg!.add(_config as MessageItem);
+ };
+ return pre;
+}, {} as any);
+
+const Message = Object.assign({
+ ...message,
+ removeAll: () => {
+ msg && msg.clear();
+ },
+});
+
+export default Message;
diff --git a/src/components/message/src/instance.ts b/src/components/message/src/instance.ts
new file mode 100644
index 0000000..7fef54a
--- /dev/null
+++ b/src/components/message/src/instance.ts
@@ -0,0 +1,77 @@
+import { AppContext, Ref, createVNode, reactive, ref, render } from "vue";
+import Message from "./messageList.vue";
+import { MessageItem } from "./type";
+import { arrayIndexOf, deepClone } from "@/utils/index";
+
+class MessageManager {
+ private mask: HTMLElement = document.createElement("div");
+ private messageList: Ref
;
+
+ constructor(appContext?: AppContext) {
+ this.messageList = ref([]);
+ this.mask.setAttribute("class", `bp-mask-message`);
+
+ const vm = createVNode(Message, {
+ list: this.messageList.value,
+ onRemove: this.remove,
+ });
+
+ if (appContext) {
+ vm.appContext = appContext;
+ }
+ render(vm, this.mask);
+ document.body.appendChild(this.mask);
+ }
+
+ /**
+ * 添加消息提示
+ * @param {MessageItem} config
+ * @returns
+ */
+ add = (config: MessageItem) => {
+ const id = config.id ?? `_bp_message_${Math.random().toString(36).slice(-8)}`;
+ this.mask.setAttribute("class", `bp-mask-message bp-message-${config.position || "top"}`);
+
+ const message: MessageItem = reactive({ id, ...config });
+
+ // Check whether the message instance already exists. If has, update the message config, or push new one.
+ const updateIdx = arrayIndexOf(this.messageList.value, "id", id);
+ updateIdx !== -1 ? (this.messageList.value[updateIdx] = config) : this.messageList.value.push(message);
+
+ // Handle possible simultaneous removal cases, step up 200ms to make the removal visual experience better.
+ const len = this.messageList.value.length;
+ if (len > 1 && this.messageList.value[len - 1]?.duration === message.duration) {
+ message.duration = message.duration ?? 3000 + 200 * len;
+ }
+
+ return {
+ remove: () => this.remove(id),
+ };
+ };
+
+ /**
+ * 移除消息提示
+ * @param {string} id 消息id
+ */
+ remove = (id: string) => {
+ for (let i = 0; i < this.messageList.value.length; i++) {
+ const { id: itemId } = this.messageList.value[i];
+
+ if (id === itemId) {
+ this.messageList.value.splice(i, 1);
+ break;
+ }
+ }
+ };
+
+ /** 清除消息列表 */
+ clear = () => {
+ const arr = deepClone(this.messageList.value);
+ for (let i = 0; i < arr.length; i++) {
+ const element = arr[i];
+ this.remove(element.id);
+ }
+ };
+}
+
+export default MessageManager;
diff --git a/src/components/message/src/message.vue b/src/components/message/src/message.vue
new file mode 100644
index 0000000..fcdd596
--- /dev/null
+++ b/src/components/message/src/message.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
+
diff --git a/src/components/message/src/messageList.vue b/src/components/message/src/messageList.vue
new file mode 100644
index 0000000..ff4d70a
--- /dev/null
+++ b/src/components/message/src/messageList.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/message/src/style/message.css b/src/components/message/src/style/message.css
new file mode 100644
index 0000000..8f6ebbb
--- /dev/null
+++ b/src/components/message/src/style/message.css
@@ -0,0 +1,188 @@
+/* @message-cls: ~"@{prefix}-message"; */
+
+.bp-mask-message {
+ position: fixed;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ z-index: 999;
+}
+
+.bp-message-bottom {
+ bottom: 0;
+}
+.bp-message-bottom .bp-message-list {
+ bottom: 40px;
+}
+
+.bp-message-top {
+ top: 0;
+}
+.bp-message-top .bp-message-list {
+ top: 40px;
+}
+.bp-message-list .bp-message {
+ min-width: 80px;
+ width: max-content;
+ height: 46px;
+ border-radius: 6px;
+ padding: 0 18px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 14px;
+ line-height: 1;
+ text-align: center;
+ list-style: none;
+ overflow: hidden;
+ /* .no-select; */
+}
+.bp-message-content {
+ font-size: 14px;
+ font-weight: normal;
+ letter-spacing: 0.2px;
+ color: #262626;
+}
+
+.bp-message-close {
+ margin-left: 6px;
+}
+.bp-icon {
+ width: 18px;
+ height: 18px;
+ fill: #8c8c8c;
+ cursor: pointer;
+ padding: 2px;
+ border-radius: 10px;
+ transition: all 0.2s ease;
+}
+.bp-icon &:hover {
+ fill: #434343;
+ background-color: #f5f5f5;
+ transition: all 0.2s ease;
+}
+.icon-success .bp-icon {
+ fill: #52c41a;
+}
+.icon-error .bp-icon {
+ fill: #f5222d;
+}
+.icon-warning .bp-icon {
+ fill: #fa8c16;
+}
+.bp-message-list .bp-message-icon {
+ width: 18px;
+ height: 18px;
+ margin-right: 6px;
+}
+.icon-loading .bp-icon {
+ display: inline-block;
+ fill: #1677ff;
+ animation: rotating 0.8s linear infinite;
+ -webkit-animation: rotating 0.8s linear infinite;
+}
+
+.bp-message-list .bp-message-text,
+.bp-message-list .bp-message-loading {
+ background: #ffffff;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ border: 1px solid #f0f0f0;
+}
+.bp-message-loading .bp-message-content {
+ color: #262626;
+}
+.bp-message-list .bp-message-success {
+ background-color: #edfceb;
+ border: 1px solid #209124;
+ box-shadow: 0 4px 12px rgba(32, 145, 36, 0.1);
+}
+.bp-message-success .bp-message-content {
+ color: #209124;
+}
+.bp-message-list .bp-message-warning {
+ background-color: #fff7e6;
+ border: 1px solid #d46b08;
+ box-shadow: 0 4px 12px rgba(212, 107, 8, 0.1);
+}
+.bp-message-warning .bp-message-content {
+ color: #d46b08;
+}
+.bp-message-error {
+ background-color: #fff1f0;
+ border: 1px solid #cf1322;
+ box-shadow: 0 4px 12px rgba(207, 19, 43, 0.1);
+}
+.bp-message-error .bp-message-content {
+ color: #cf1322;
+}
+.bp-message-plain {
+ background: #ffffff;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ border: 1px solid #f0f0f0;
+}
+.bp-message-plain-content {
+ color: #262626;
+}
+
+.bp-message-plain-close .bp-icon {
+ fill: #8c8c8c;
+}
+.bp-message-plain-close .bp-icon &:hover {
+ fill: #595959;
+}
+.bp-message-plain-close .bp-icon &::before {
+ background-color: #f0f0f0;
+}
+.bp-message-plain-icon .bp-icon {
+ width: 18px;
+ height: 18px;
+}
+
+.bp-message-plain-icon .icon-success .bp-icon {
+ fill: #52c41a;
+}
+.bp-message-plain-icon .icon-error .bp-icon {
+ fill: #f5222d;
+}
+.bp-message-plain-icon .icon-warning .bp-icon {
+ fill: #fa8c16;
+}
+.bp-message-plain-icon .icon-loading .bp-icon {
+ fill: #1677ff;
+}
+.bp-message-list {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: nowrap;
+}
+
+@keyframes rotating {
+ from {
+ transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ -webkit-transform: rotate(365deg);
+ }
+}
+
+.fademsg-move,
+.fademsg-enter-active,
+.fademsg-leave-active {
+ transition: all 0.2s cubic-bezier(0, 0, 1, 1);
+}
+.fademsg-enter-from,
+.fademsg-leave-to {
+ opacity: 0;
+}
+.fademsg-leave-active {
+ position: absolute;
+}
diff --git a/src/components/message/src/type.ts b/src/components/message/src/type.ts
new file mode 100644
index 0000000..300127f
--- /dev/null
+++ b/src/components/message/src/type.ts
@@ -0,0 +1,19 @@
+/** 消息提示位置 */
+export type MessagePosition = "top" | "bottom";
+
+/** 消息类型 */
+export type MessageType = "text" | "success" | "warning" | "error" | "loading";
+
+/** 传入的消息类型 */
+export type MessageInstance = string | MessageItem;
+
+export interface MessageItem {
+ id: string;
+ type?: MessageType;
+ content: string;
+ duration?: number;
+ closeable?: boolean;
+ plain: boolean;
+ position: MessagePosition;
+ onClose?: Function;
+}
diff --git a/src/components/useUiNotification/index.ts b/src/components/useUiNotification/index.ts
new file mode 100644
index 0000000..a020a18
--- /dev/null
+++ b/src/components/useUiNotification/index.ts
@@ -0,0 +1,70 @@
+import { computed, reactive, useContext } from "vue";
+import type { UiNotification, UseUiNotificationInterface } from "./useUiNotification";
+
+interface Notifications {
+ notifications: UiNotification[];
+}
+
+const state = reactive({
+ notifications: [],
+});
+const maxVisibleNotifications = 3;
+const timeToLive = 3000;
+
+/**
+ * Allows managing and showing notifications to the user.
+ *
+ * See the {@link UseUiNotificationInterface} for a list of methods and values available in this composable.
+ */
+export function useUiNotification(): UseUiNotificationInterface {
+ const { app } = useContext();
+ // @ts-ignore
+ const cookieMessage = app.$vsf.$magento.config.state.getMessage();
+
+ const send = (notification: UiNotification) => {
+ const id = Symbol("id");
+
+ const dismiss = () => {
+ const index = state.notifications.findIndex((n) => n.id === id);
+
+ if (index !== -1) state.notifications.splice(index, 1);
+
+ // @ts-ignore
+ app.$vsf.$magento.config.state.removeMessage();
+ };
+
+ const newNotification = {
+ ...notification,
+ id,
+ dismiss,
+ };
+
+ if (!state.notifications.some((stateNotification) => stateNotification.message === notification.message)) {
+ state.notifications.push(newNotification);
+ }
+
+ if (state.notifications.length > maxVisibleNotifications) {
+ state.notifications.shift();
+ }
+
+ if (!notification.persist) {
+ setTimeout(dismiss, timeToLive);
+ }
+ };
+
+ if (cookieMessage) {
+ // @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+ send(cookieMessage);
+ // @ts-ignore
+ app.$vsf.$magento.config.state.removeMessage();
+ }
+
+ return {
+ send,
+ notifications: computed(() => state.notifications),
+ };
+}
+
+export default useUiNotification;
+export * from "./useUiNotification";
diff --git a/src/components/useUiNotification/useUiNotification.ts b/src/components/useUiNotification/useUiNotification.ts
new file mode 100644
index 0000000..abaac08
--- /dev/null
+++ b/src/components/useUiNotification/useUiNotification.ts
@@ -0,0 +1,32 @@
+import type { ComputedRef } from "vue";
+
+export type UiNotificationType = "secondary" | "info" | "success" | "warning" | "danger";
+
+export interface UiNotification {
+ message: string;
+ title?: string;
+ action?: {
+ text: string;
+ onClick(...args: any): void;
+ };
+ type: UiNotificationType;
+ icon: string;
+ persist?: boolean;
+ id: symbol;
+ dismiss?: () => void;
+}
+
+/**
+ * Data and methods returned from the {@link useUiNotification|useUiNotification()} composable
+ */
+export interface UseUiNotificationInterface {
+ /**
+ * Displays the notification in the UI
+ */
+ send(notification: UiNotification): void;
+
+ /**
+ * Contains notifications added using the `send` method
+ */
+ notifications: ComputedRef;
+}
diff --git a/src/main.ts b/src/main.ts
index e94e0ea..954dd2c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,6 @@
import { createApp } from "vue";
import "./style.css";
+import "./components/message/src/style/message.css";
import App from "./App.vue";
import { createPinia } from "pinia";
diff --git a/src/types/cart.d.ts b/src/types/cart.d.ts
index 8a9476c..c28d802 100644
--- a/src/types/cart.d.ts
+++ b/src/types/cart.d.ts
@@ -7,8 +7,8 @@ export interface CartItem {
// specs: any[];
picture: string;
price: string;
- nowPrice: string;
- nowOriginalPrice: string;
+ // nowPrice: string;
+ // nowOriginalPrice: string;
selected: boolean;
stock: number;
count: number;
diff --git a/src/types/goods.d.ts b/src/types/goods.d.ts
index 51a92f0..900b309 100644
--- a/src/types/goods.d.ts
+++ b/src/types/goods.d.ts
@@ -64,7 +64,7 @@ export interface GoodsDetail {
properties: ProductProperty[];
};
isPreSale: boolean;
- isCollect: boolean;
+ isCollect: boolean | null;
recommends: SimilarProduct[] | null;
// userAddresses: any[] | null; // 这里可以根据具体地址对象的结构进一步定义类型
similarProducts: SimilarProduct[];
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 2f94e4a..7d7e6c2 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -20,3 +20,52 @@ export const getPriceRange = (min_amount: string, max_amount: string) => {
if (min_amount != max_amount) return `$${min_amount} – $${max_amount}`;
else return `$${min_amount}`;
};
+
+/**
+ * 找出数组里是否含有某个属性值
+ * @param arr
+ * @param field
+ * @param fieldValue
+ * @returns number
+ */
+export function arrayIndexOf(arr: unknown[], field: string, fieldValue: unknown) {
+ for (let i = 0; i < arr.length; i++) {
+ const element: any = arr[i];
+ if (element[field] === fieldValue) return i;
+ }
+
+ return -1;
+}
+/**
+ * 对象深拷贝
+ * @param tSource
+ * @returns
+ */
+export function deepClone(tSource: T, tTarget?: Record | T): T {
+ if (isArray(tSource)) {
+ tTarget = tTarget || [];
+ } else {
+ tTarget = tTarget || {};
+ }
+ for (const key in tSource) {
+ if (Object.prototype.hasOwnProperty.call(tSource, key)) {
+ // eslint-disable-next-line valid-typeof
+ if (typeof tSource[key] === "object" && typeof tSource[key] !== null) {
+ tTarget[key] = isArray(tSource[key]) ? [] : ({} as any);
+ deepClone(tSource[key], tTarget[key]);
+ } else {
+ tTarget[key] = tSource[key];
+ }
+ }
+ }
+ return tTarget as T;
+}
+
+/**
+ * 判断对象是否为数组
+ * @param obj
+ * @returns
+ */
+export const isArray = (obj: any) => {
+ return obj && typeof obj == "object" && obj instanceof Array;
+};
diff --git a/src/views/cart/Index.vue b/src/views/cart/Index.vue
index 5ceef21..30c0ef9 100644
--- a/src/views/cart/Index.vue
+++ b/src/views/cart/Index.vue
@@ -10,7 +10,7 @@
-
+
|
- HK$272.95 |
+ {{ item.price }} |
|
@@ -157,4 +157,9 @@ import { SfIconFavorite } from "@storefront-ui/vue";
import { ref } from "vue";
// https://tailwind.nodejs.cn/docs/table-layout
const count = ref(1);
+
+import useStore from "@/stores";
+
+const { cart } = useStore();
+cart.getCartList();
diff --git a/src/views/product/Index.vue b/src/views/product/Index.vue
index 310462b..06c62b6 100644
--- a/src/views/product/Index.vue
+++ b/src/views/product/Index.vue
@@ -77,7 +77,7 @@
-
+
About Products
@@ -179,9 +179,12 @@ import { ref, onMounted } from "vue";
import { SfButton, useId } from "@storefront-ui/vue";
import { clamp } from "@storefront-ui/shared";
import { useCounter } from "@vueuse/core";
-import { goods as _product } from "../../assets/json/goods";
+import { goods as _product } from "../../assets/json/goods1";
import ProductSpecs from "./ProductSpecs/index.vue";
import { useRouter } from "vue-router";
+import useStore from "@/stores";
+import type { GoodsDetail, CartItem } from "@/types";
+import Message from "@/components/message/index";
const min = ref(1);
const max = ref(10);
const { count, inc, dec, set } = useCounter(1, {
@@ -196,10 +199,11 @@ function handleOnChange(event: Event) {
const router = useRouter();
const handleClick = () => {
- console.log("handleClick");
+ Message.text("这是一条文本类型的消息提示");
+ // console.log("handleClick");
- const slug = "warm-winter-cozy-washable-dog-house";
- router.push({ path: `/product/${slug}` });
+ // const slug = "warm-winter-cozy-washable-dog-house";
+ // router.push({ path: `/product/${slug}` });
};
import { SfIconChevronLeft, SfIconChevronRight, type SfScrollableOnDragEndData } from "@storefront-ui/vue";
import { unrefElement, useIntersectionObserver } from "@vueuse/core";
@@ -284,7 +288,6 @@ const assignRef = (el: Element | ComponentPublicInstance | null, index: number)
}
};
-import type { GoodsDetail } from "@/types/goods";
const goods = ref
();
onMounted(async () => {
// console.log(JSON.stringify(_product));
@@ -294,18 +297,51 @@ onMounted(async () => {
goods.value = _product;
});
+// 获取 XtxSku 组件选中的商品信息
+const skuId = ref("");
+const attrsText = ref("");
// const addToBag = () => {};
-const changeSku = () => {
+const changeSku = (value: any) => {
// 🔔存储 skuId 用于加入购物车
- // skuId.value = value.skuId || "";
- // // 存储选中规格文本
- // attrsText.value = value.specsText;
- // // console.log("当前选择的SKU为信息为", value);
- // if (goods.value && value.skuId) {
- // // 根据选中规格,更新商品库存,销售价格,原始价格
- // goods.value.inventory = value.inventory;
- // goods.value.price = value.price;
- // goods.value.oldPrice = value.oldPrice;
- // }
+ skuId.value = value.skuId || "";
+ // 存储选中规格文本
+ attrsText.value = value.specsText;
+ // console.log("当前选择的SKU为信息为", value);
+ if (goods.value && value.skuId) {
+ // 根据选中规格,更新商品库存,销售价格,原始价格
+ goods.value.inventory = value.inventory;
+ // goods.value.price = value.price;
+ // goods.value.oldPrice = value.oldPrice;
+ }
+};
+const { cart } = useStore();
+// 加入购物按钮点击
+const addCart = () => {
+ // 没有 skuId,提醒用户并退出函数
+ if (!skuId.value) {
+ // return message({ type: "warn", text: "请选择完整商品规则~" });
+ }
+ if (!goods.value) return;
+ // Partial 泛型工具类型 全部 转可选
+ const cartItem: CartItem = {
+ // 🚨🚨 注意数据收集字段名很多坑,小心操作
+ // 第一部分:商品详情中有的
+ id: goods.value.id, // 商品id
+ name: goods.value.name, // 商品名称
+ picture: goods.value.mainPictures[0], // 图片
+ // price: goods.value.price, // 旧价格
+ // nowPrice: goods.value.price, // 新价格
+ stock: goods.value.inventory, // 库存
+ // 第二部分:商品详情中没有的,自己通过响应式数据收集
+ count: count.value, // 商品数量
+ skuId: skuId.value, // skuId
+ attrsText: attrsText.value, // 商品规格文本
+ // 第三部分:设置默认值即可
+ selected: true, // 默认商品选中
+ isEffective: true, // 默认商品有效
+ } as CartItem;
+ console.log("😭 cartItem 数据终于准备完毕了", cartItem);
+ // 调用加入购物车接口
+ cart.addCart(cartItem);
};
diff --git a/src/views/product/ProductSpecs/index.vue b/src/views/product/ProductSpecs/index.vue
index 04c0317..994001f 100644
--- a/src/views/product/ProductSpecs/index.vue
+++ b/src/views/product/ProductSpecs/index.vue
@@ -1,5 +1,5 @@
-
-
-
@@ -112,16 +68,6 @@