[FEAT] General flavor 多品項購物車(Part 1)

移植 Luzui v2 Kotlin MVVM 購物車到 General flavor,接 TaiwanStar 既有
結帳鏈(BuyDialog → 付款 → DispatchDialog → MQTT 結案)、沿用 VMC 出貨。

- build:啟用 Kotlin/lifecycle/BaseRecyclerViewAdapterHelper(全域 classpath,
  cart .kt 僅在 src/General 編譯,不影響 Chengwai/Cmuh 行為)
- 移植至 src/General:CartViewModel/CartOperationAdapter/CartOperationResult/
  SafeCountdownUtil/UnibuyHelper/ShoppingCartFragment + layout/drawable/三語 strings
  - 移除 Luzui 的 updateLocalGoods(FLOWER/DAO)、DialogXManager、HttpAPI、格子機依賴
  - 結帳改 convertToBuyStructure() + SaleFlowHandler.onBuyRequested()
- 共用檔最小改動(對 Chengwai/Cmuh 零行為影響):
  - SlotStructure 加 cartSelectedCount
  - FontendActivity 加 openFragment()/backToVmc()、購物車浮鈕改走 onShoppingCartRequested()
  - RecyclerVmcBuyListAdpter 點商品改走 flavor hook handleProductSelected()
  - 三個 SaleFlowHandler 加 onShoppingCartRequested()/handleProductSelected()
    (General:加入購物車;Chengwai/Cmuh:維持原行為)

三 flavor 編譯通過、General APK 打包成功。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-10 08:45:11 +08:00
parent 5f480d6cb6
commit cfc802dbb5
23 changed files with 970 additions and 3 deletions

View File

@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
// =========================================================================
@ -111,6 +112,10 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '11'
}
buildFeatures {
viewBinding true
buildConfig true
@ -146,4 +151,11 @@ dependencies {
implementation 'io.netty:netty-codec-http:4.1.94.Final'
implementation 'io.netty:netty-handler:4.1.94.Final'
implementation 'com.google.code.gson:gson:2.10.1'
// Kotlin / MVVM General flavor
implementation libs.kotlin.stdlib
implementation libs.lifecycle.viewmodel.ktx
implementation libs.lifecycle.livedata.ktx
implementation libs.lifecycle.runtime.ktx
implementation libs.brvah
}

View File

@ -32,6 +32,16 @@ public class SaleFlowHandler {
new MemberCardPickupDialog(context, handlerMain).show();
}
public void onShoppingCartRequested() {
// 晟崴版維持原行為購物車浮鈕等同進入購買流程
onBuyRequested();
}
/** 晟崴版不消化商品點擊,交回 adapter 既有流程(員工卡取貨)。 */
public boolean handleProductSelected(com.unibuy.smartdevice.structure.SlotStructure slot) {
return false;
}
public void onBarcodeReceived(String rawData) {
// 晟崴版不處理掃碼
}

View File

@ -87,6 +87,16 @@ public class SaleFlowHandler {
// 中國醫版沒有購物車付款此方法不執行
}
public void onShoppingCartRequested() {
// 中國醫版維持原行為不處理購物車浮鈕
onBuyRequested();
}
/** 中國醫版不消化商品點擊,交回 adapter 既有流程。 */
public boolean handleProductSelected(com.unibuy.smartdevice.structure.SlotStructure slot) {
return false;
}
public void onBarcodeReceived(String rawData) {
// 若是經由其他方式傳入的 Barcode 可在此處直接處理
try {

View File

@ -1,12 +1,17 @@
package com.unibuy.smartdevice.ui;
import android.content.Context;
import com.blankj.utilcode.util.ToastUtils;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
import com.unibuy.v2.UnibuyHelper;
import com.unibuy.v2.ui.ShoppingCartFragment;
/**
* General (通用版) 銷售流程處理器
* 點選商品/購物車時彈出標準付款 Dialog (BuyDialog)
* 結帳時彈出標準付款 Dialog (BuyDialog)購物車浮鈕則開啟多品項購物車 Fragment
*/
public class SaleFlowHandler {
@ -14,6 +19,9 @@ public class SaleFlowHandler {
protected final SaleFlowCallback callback;
protected final HandlerMain handlerMain;
/** 多品項購物車 Fragment懶建立重複開啟時沿用同一實例。 */
private ShoppingCartFragment cartFragment;
public SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
this.context = context;
this.handlerMain = handlerMain;
@ -32,6 +40,27 @@ public class SaleFlowHandler {
new BuyDialog(context, handlerMain).show();
}
/**
* 點選 VMC 商品加入多品項購物車並停留商品列表不做取貨口檢查出貨時才檢查
* @return true 表示已消化此次點擊General 一律 true
*/
public boolean handleProductSelected(SlotStructure slot) {
UnibuyHelper.INSTANCE.getCartViewModel().addToCart(slot);
ToastUtils.showShort(R.string.cart_added);
return true;
}
/** 購物車浮鈕:開啟多品項購物車 Fragment。 */
public void onShoppingCartRequested() {
if (!(context instanceof FontendActivity)) {
return;
}
if (cartFragment == null) {
cartFragment = new ShoppingCartFragment(handlerMain);
}
((FontendActivity) context).openFragment(cartFragment);
}
public void onBarcodeReceived(String rawData) {
// 通用版不處理掃碼
}

View File

@ -0,0 +1,35 @@
package com.unibuy.v2
import com.blankj.utilcode.util.ActivityUtils
import com.blankj.utilcode.util.AppUtils
import com.unibuy.smartdevice.ui.FontendActivity
import com.unibuy.v2.vm.CartViewModel
/**
* v2 購物車的橋接 helper Luzui 移植並裁剪
*
* Luzui 差異
* - getCartViewModel() 改為自持 singleton不綁 FontendActivity ViewModelProvider
* 避免修改 main 共用的 FontendActivity其無 cartViewModel 欄位
* - 移除 dismissAllAlertDialogs()/getSPStrNotNull()前者依賴 DialogAbstract.dialogInstances
* TaiwanStar private static 無法存取後者購物車未使用
*/
object UnibuyHelper {
private val cartViewModel: CartViewModel = CartViewModel()
fun getCartViewModel(): CartViewModel = cartViewModel
fun getFontendActivity(): FontendActivity {
if (ActivityUtils.getTopActivity() is FontendActivity) {
return ActivityUtils.getTopActivity() as FontendActivity
} else if (ActivityUtils.isActivityExists(AppUtils.getAppPackageName(), FontendActivity::class.java.simpleName)) {
ActivityUtils.getActivityList().forEach { activity ->
if (activity is FontendActivity) {
return activity
}
}
}
return FontendActivity()
}
}

View File

@ -0,0 +1,55 @@
package com.unibuy.v2.adapter
import android.widget.ImageView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.unibuy.smartdevice.R
import com.unibuy.smartdevice.exception.LogsEmptyException
import com.unibuy.smartdevice.structure.SlotStructure
import com.unibuy.smartdevice.ui.tools.ImageGlide
import com.unibuy.v2.UnibuyHelper
/**
* 購物車品項清單 Adapter Luzui v2 移植
* 依賴 BaseRecyclerViewAdapterHelper BaseQuickAdapter
*/
class CartOperationAdapter(layoutResId: Int, data: MutableList<SlotStructure>) :
BaseQuickAdapter<SlotStructure, BaseViewHolder>(layoutResId, data) {
init {
addChildClickViewIds(R.id.buttonMinus, R.id.buttonPlus, R.id.buttonWaste, R.id.layout_product, R.id.image_product)
}
override fun convert(holder: BaseViewHolder, item: SlotStructure) {
val result = UnibuyHelper.getCartViewModel().createResult(item)
// 多國語商品名稱
val lang = java.util.Locale.getDefault().language // "zh" / "en" / "ja"
val name = when {
lang.equals("en", ignoreCase = true) ->
item.product.productNameEn
lang.equals("ja", ignoreCase = true) ||
lang.equals("jp", ignoreCase = true) ->
item.product.productNameJp
else ->
item.product.productName
}.let { localizedName ->
if (localizedName.isNullOrBlank()) {
item.product.productName
} else {
localizedName
}
}
holder.setText(R.id.textProductName, name)
.setText(R.id.totalPriceTextView, "NT$ ${result.currentItemPrice}")
.setText(R.id.textQuantity, "${result.currentItemCount}")
.setText(R.id.textPrice, "NT$ ${item.product.realPrice}")
val imageProductPicture = holder.getView<ImageView>(R.id.image_product)
try {
ImageGlide(imageProductPicture.context).fileload(item.product.productImg, imageProductPicture)
} catch (e: LogsEmptyException) {
e.printStackTrace()
}
}
}

View File

@ -0,0 +1,9 @@
package com.unibuy.v2.bean
// 購物車操作結果資料類
data class CartOperationResult(
val totalPrice: Int, // 購物車總價
val totalCount: Int, // 購物車總數量
val currentItemCount: Int, // 當前商品數量
val currentItemPrice: Int // 當前商品價格
)

View File

@ -0,0 +1,240 @@
package com.unibuy.v2.ui;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.ClickUtils;
import com.blankj.utilcode.util.ToastUtils;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.unibuy.smartdevice.FragmentAbstract;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.FragmentShoppingCartBinding;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.ui.FontendActivity;
import com.unibuy.v2.UnibuyHelper;
import com.unibuy.v2.adapter.CartOperationAdapter;
import com.unibuy.v2.bean.CartOperationResult;
import com.unibuy.v2.utils.SafeCountdownUtil;
import java.util.List;
/**
* General flavor 多品項購物車 Fragment Luzui v2 移植並適配
*
* Luzui 差異
* - 移除 HttpAPI / DialogXManager / FlowerFragment / 格子機相關邏輯
* - 加購 VMC 商品列表結帳改為 convertToBuyStructure() + SaleFlowHandler.onBuyRequested()
* 交棒給 TaiwanStar 既有結帳鏈BuyDialog 付款 DispatchDialog MQTT 結案
* - 移除 MyApp.setShoppingCartFragmentInstanceTaiwanStar 無此 API
*/
public class ShoppingCartFragment extends FragmentAbstract {
private FragmentShoppingCartBinding binding;
private CartOperationAdapter cartAdapter;
private CartOperationResult allCartItems;
private SafeCountdownUtil countdownUtil;
private static final int DEBOUNCE_DURATION = 500;
private static final int COUNTDOWN_DURATION = 180;
/** 回傳當前倒數秒數(未初始化回 -1。 */
public int getCurrentCountdownSeconds() {
if (countdownUtil != null) {
return countdownUtil.getCurrentSeconds();
}
return -1;
}
@Override
protected String setName() {
return getString(R.string.shopping_cart_title);
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<?> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getCtx(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
// 購物車本身不需處理額外指令
}
};
}
public ShoppingCartFragment() {
super(null);
}
public ShoppingCartFragment(HandlerMain srcHandlerMain) {
super(srcHandlerMain);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentShoppingCartBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupUI();
}
private void setupUI() {
setupCartRecyclerView();
setupClickListeners();
refreshTitleView();
initCountdownUtil();
}
private void initCountdownUtil() {
if (countdownUtil == null) {
countdownUtil = new SafeCountdownUtil(getViewLifecycleOwner().getLifecycle());
countdownUtil.setCountdownListener(new SafeCountdownUtil.CountdownListener() {
@Override
public void onTick(int seconds) {
updateCountdownText(seconds);
}
@Override
public void onFinish() {
if (isAdded() && getActivity() != null) {
getActivity().runOnUiThread(() -> clearCartAndBack());
}
}
});
}
}
private void updateCountdownText(int seconds) {
if (!isAdded() || getActivity() == null) {
return;
}
getActivity().runOnUiThread(() -> {
if (binding != null && binding.cancelText != null) {
binding.cancelText.setText(getString(R.string.cancel_button_format, seconds));
}
});
}
private void handleCartItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {
try {
SlotStructure slot = (SlotStructure) adapter.getData().get(position);
int viewId = view.getId();
if (viewId == R.id.buttonMinus) {
UnibuyHelper.INSTANCE.getCartViewModel().removeFromCart(slot);
adapter.notifyItemChanged(position);
} else if (viewId == R.id.buttonPlus) {
UnibuyHelper.INSTANCE.getCartViewModel().addToCart(slot);
adapter.notifyItemChanged(position);
} else if (viewId == R.id.buttonWaste) {
UnibuyHelper.INSTANCE.getCartViewModel().removeItemFromCart(slot);
adapter.remove(position);
// 購物車清空後返回 VMC 商品列表
if (UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot().isEmpty()) {
backToVmc();
}
}
// layout_product / image_product 的商品詳情 popup Luzui DialogXManagerGeneral 不採用
refreshTitleView();
} catch (Exception e) {
ToastUtils.showLong(getString(R.string.data_error));
e.printStackTrace();
}
}
private void setupClickListeners() {
// 加購商品回到 VMC 商品列表繼續挑選
ClickUtils.applySingleDebouncing(binding.addMoreItemsButton, DEBOUNCE_DURATION, v -> backToVmc());
// 前往結帳寫入 buyList 後交棒給既有結帳鏈
ClickUtils.applySingleDebouncing(binding.confirmButton, DEBOUNCE_DURATION, v -> {
if (UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot().isEmpty()) {
ToastUtils.showLong(getString(R.string.cart_cannot_decrease));
return;
}
UnibuyHelper.INSTANCE.getCartViewModel().convertToBuyStructure();
FontendActivity activity = getFontendActivity();
if (activity != null && activity.getSaleFlowHandler() != null) {
activity.getSaleFlowHandler().onBuyRequested();
}
});
// 取消清空購物車並返回 VMC
ClickUtils.applySingleDebouncing(binding.cancelButton, DEBOUNCE_DURATION, v -> clearCartAndBack());
}
private void setupCartRecyclerView() {
List<SlotStructure> cartItems = UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot();
cartAdapter = new CartOperationAdapter(R.layout.recycler_cart_operation, cartItems);
cartAdapter.setOnItemChildClickListener(this::handleCartItemClick);
binding.shoppingCartRecyclerView.setAdapter(cartAdapter);
}
private void refreshTitleView() {
allCartItems = UnibuyHelper.INSTANCE.getCartViewModel().getAllCartItems();
binding.totalItemsTextView.setText(getString(R.string.total_items_format, allCartItems.getTotalCount()));
binding.totalAmountTextView.setText(getString(R.string.total_amount_format, allCartItems.getTotalPrice()));
}
/** 清空購物車並切回 VMC 商品列表。 */
private void clearCartAndBack() {
UnibuyHelper.INSTANCE.getCartViewModel().clearCart();
backToVmc();
}
private void backToVmc() {
FontendActivity activity = getFontendActivity();
if (activity != null) {
activity.backToVmc();
}
}
@Nullable
private FontendActivity getFontendActivity() {
if (getActivity() instanceof FontendActivity) {
return (FontendActivity) getActivity();
}
return null;
}
@Override
public void startOnSwitchFragment() {
setupUI();
countdownUtil.startOrReset(COUNTDOWN_DURATION);
}
@Override
public void stopOnSwitchFragment() {
if (countdownUtil != null) {
countdownUtil.stop();
}
}
public void countdownReset() {
countdownUtil.startOrReset(COUNTDOWN_DURATION);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@ -0,0 +1,76 @@
package com.unibuy.v2.utils
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import java.util.Timer
import java.util.TimerTask
/**
* Fragment 生命週期綁定的安全倒數計時器 Luzui v2 移植
*/
class SafeCountdownUtil(private val lifecycle: Lifecycle) : DefaultLifecycleObserver {
private var timer: Timer? = null
private var currentTime = 0
private var listener: CountdownListener? = null
private var lastCallTime: Long = 0
fun getCurrentSeconds(): Int {
return currentTime
}
init {
lifecycle.addObserver(this)
}
interface CountdownListener {
fun onTick(seconds: Int)
fun onFinish()
}
fun setCountdownListener(listener: CountdownListener) {
this.listener = listener
}
// 安全啟動/重置方法(帶防抖)
fun startOrReset(seconds: Int) {
synchronized(this) {
val now = System.currentTimeMillis()
if (now - lastCallTime < 200) {
return
}
lastCallTime = now
timer?.cancel()
currentTime = seconds
listener?.onTick(currentTime)
timer = Timer().apply {
schedule(object : TimerTask() {
override fun run() {
if (currentTime > 0) {
currentTime--
listener?.onTick(currentTime)
} else {
stop()
listener?.onFinish()
}
}
}, 1000, 1000)
}
}
}
fun stop() {
synchronized(this) {
timer?.cancel()
timer = null
}
}
override fun onDestroy(owner: LifecycleOwner) {
stop()
lifecycle.removeObserver(this)
listener = null
}
}

View File

@ -0,0 +1,148 @@
package com.unibuy.v2.vm
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.blankj.utilcode.util.ToastUtils
import com.unibuy.smartdevice.MyApp
import com.unibuy.smartdevice.R
import com.unibuy.smartdevice.structure.BuyStructure
import com.unibuy.smartdevice.structure.SlotStructure
import com.unibuy.v2.UnibuyHelper
import com.unibuy.v2.bean.CartOperationResult
/**
* 多品項購物車 ViewModel Luzui v2 移植
*
* Luzui 版差異
* - 移除 updateLocalGoods()Luzui SlotField.FLOWER + SlotsDao 在出貨後扣本地庫存
* TaiwanStar VMC 貨道機扣庫存由既有 DispatchDialog / MQTT 結案負責購物車不重複處理
* - 寫死中文改用 R.string.cart_max_items
*/
class CartViewModel : ViewModel() {
private val _cartItems = MutableLiveData<List<SlotStructure>>(emptyList())
val cartItems: LiveData<List<SlotStructure>> = _cartItems
val freeGoods: MutableList<BuyStructure> = mutableListOf()
/** 把購物車內容寫入 MyApp.buyList交棒給既有結帳鏈BuyDialog → 付款 → DispatchDialog → MQTT。 */
fun convertToBuyStructure() {
MyApp.getInstance().apply {
buyList.clear()
// 1. 免費商品來店禮General 不做時恆空)
buyList += freeGoods
// 2. 購物車商品
buyList += UnibuyHelper.getCartViewModel()
.getCartItemsSnapshot()
.map { cartItem ->
BuyStructure(cartItem.field, cartItem.slot, cartItem.product, cartItem.cartSelectedCount)
}
}
}
/** 購物車最多 8 件 */
fun hasMinimumCartItems(): Boolean {
return getAllCartItems().totalCount >= 8
}
fun getCartItemsSnapshot(): List<SlotStructure> = _cartItems.value ?: emptyList()
// 添加商品到購物車(以 field + slot + productID 唯一識別)
fun addToCart(slot: SlotStructure): CartOperationResult {
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
val item = currentItems.find {
it.field == slot.field &&
it.slot == slot.slot &&
it.product?.productID == slot.product?.productID
}
return if (item == null) {
if (hasMinimumCartItems()) {
ToastUtils.showLong(R.string.cart_max_items)
return createResult(null)
}
slot.cartSelectedCount = 1
val newList = currentItems.toMutableList().apply { add(slot) }
_cartItems.value = newList
createResult(slot)
} else if (item.cartSelectedCount >= item.count) {
ToastUtils.showLong(R.string.cart_out_of_stock)
createResult(item)
} else {
if (UnibuyHelper.getCartViewModel().hasMinimumCartItems()) {
ToastUtils.showLong(R.string.cart_max_items)
createResult(item)
} else {
item.cartSelectedCount++
_cartItems.value = currentItems
createResult(item)
}
}
}
// 從購物車減少一件
fun removeFromCart(slot: SlotStructure): CartOperationResult {
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
val item = currentItems.find {
it.field == slot.field &&
it.slot == slot.slot &&
it.product?.productID == slot.product?.productID
} ?: return CartOperationResult(0, 0, 0, 0)
return if (item.cartSelectedCount > 1) {
item.cartSelectedCount--
_cartItems.value = currentItems
createResult(item)
} else {
ToastUtils.showLong(R.string.cart_cannot_decrease)
createResult(item)
}
}
// 清空購物車
fun clearCart(): CartOperationResult {
val result = createResult(null)
_cartItems.value = emptyList()
return result.copy(
totalPrice = 0,
totalCount = 0,
currentItemCount = 0,
currentItemPrice = 0
)
}
fun createResult(item: SlotStructure?): CartOperationResult {
val currentCount = item?.cartSelectedCount ?: 0
val currentPrice = (item?.product?.realPrice ?: 0) * currentCount
val currentItems = _cartItems.value ?: emptyList()
return CartOperationResult(
totalPrice = currentItems.sumOf { it.cartSelectedCount * (it.product?.realPrice ?: 0) },
totalCount = currentItems.sumOf { it.cartSelectedCount },
currentItemCount = currentCount,
currentItemPrice = currentPrice
)
}
// 移除整筆商品
fun removeItemFromCart(slot: SlotStructure): CartOperationResult {
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
val productIdToRemove = slot.product?.productID
if (productIdToRemove != null) {
currentItems.removeAll { item ->
item.product?.productID == productIdToRemove
}
}
_cartItems.value = currentItems
return getAllCartItems()
}
fun getAllCartItems(): CartOperationResult {
val currentItems = _cartItems.value ?: emptyList()
return CartOperationResult(
totalPrice = currentItems.sumOf { it.cartSelectedCount * (it.product?.realPrice ?: 0) },
totalCount = currentItems.sumOf { it.cartSelectedCount },
currentItemCount = 0,
currentItemPrice = 0
)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<!-- 购物车商品列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/shoppingCartRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="48dp"
android:layout_weight="1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#B6B6B6" />
<LinearLayout
android:id="@+id/summaryContainer"
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="horizontal"
android:paddingHorizontal="40dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal"
android:paddingHorizontal="60dp"
android:paddingVertical="12dp">
<TextView
android:id="@+id/totalItemsTextView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="40dp"
android:layout_weight="1"
android:gravity="center|end"
android:text="@string/total_items_format"
android:textColor="#767676"
android:textSize="46sp"
android:textStyle="bold" />
<TextView
android:id="@+id/totalAmountTextView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center|end"
android:text="@string/total_amount_format"
android:textColor="@color/red"
android:textSize="52sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_alignParentBottom="true"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/addMoreItemsButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#D1D1D1"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="20dp"
android:gravity="center"
android:padding="4dp"
android:text="@string/add_more_items"
android:textColor="@color/white"
android:textSize="46sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/confirmButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#0060E2"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="20dp"
android:gravity="center"
android:padding="4dp"
android:text="@string/go_to_pay"
android:textColor="@color/white"
android:textSize="46sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/cancelButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#000000"
android:gravity="center">
<TextView
android:id="@+id/cancelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="20dp"
android:gravity="center"
android:padding="4dp"
android:text="@string/cancel"
android:textColor="@color/white"
android:textSize="46sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 商品信息布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="16dp">
<ImageView
android:id="@+id/image_product"
android:layout_width="96dp"
android:layout_height="96dp" />
<LinearLayout
android:id="@+id/layout_product"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="22"
android:orientation="vertical"
android:paddingHorizontal="32dp">
<TextView
android:id="@+id/textProductName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:text="商品"
android:textColor="#000000"
android:textSize="34sp" />
<TextView
android:id="@+id/textPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$ 0"
android:textColor="#1A72DD"
android:textSize="28sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="11"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/buttonMinus"
android:layout_width="51dp"
android:layout_height="51dp"
android:src="@drawable/ic_minus" />
<TextView
android:id="@+id/textQuantity"
android:layout_width="86dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="1"
android:textColor="#1A72DD"
android:textSize="42sp" />
<ImageView
android:id="@+id/buttonPlus"
android:layout_width="51dp"
android:layout_height="51dp"
android:src="@drawable/ic_plus" />
</LinearLayout>
<ImageView
android:id="@+id/buttonWaste"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginStart="8dp"
android:src="@drawable/ic_waste" />
<TextView
android:id="@+id/totalPriceTextView"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:text="$ 0.0000"
android:textColor="#000000"
android:textSize="32sp" />
</LinearLayout>
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#939393" />
</LinearLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- General flavor multi-item shopping cart -->
<string name="shopping_cart_title">Shopping Cart</string>
<string name="add_more_items">Add More Items</string>
<string name="go_to_pay">Proceed to Checkout</string>
<string name="cancel">Cancel</string>
<string name="cancel_button_format">Cancel (%d sec)</string>
<string name="total_items_format">Items: (%d)</string>
<string name="total_amount_format">NT$ %d</string>
<string name="cart_out_of_stock">Out of Stock</string>
<string name="cart_cannot_decrease">Cannot Decrease Further</string>
<string name="cart_max_items">Up to 8 items per cart</string>
<string name="cart_added">Added to cart</string>
<string name="data_error">Data Error</string>
</resources>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- General flavor 多品目ショッピングカート -->
<string name="shopping_cart_title">ショッピングカート</string>
<string name="add_more_items">商品を追加</string>
<string name="go_to_pay">お会計へ進む</string>
<string name="cancel">キャンセル</string>
<string name="cancel_button_format">キャンセル(%d秒</string>
<string name="total_items_format">商品数:(%d)</string>
<string name="total_amount_format">NT$ %d</string>
<string name="cart_out_of_stock">在庫切れ</string>
<string name="cart_cannot_decrease">これ以上減らせません</string>
<string name="cart_max_items">カートは最大8点まで購入できます</string>
<string name="cart_added">カートに追加しました</string>
<string name="data_error">データ異常</string>
</resources>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- General flavor 多品項購物車 -->
<string name="shopping_cart_title">購物車</string>
<string name="add_more_items">加購商品</string>
<string name="go_to_pay">前往結帳</string>
<string name="cancel">取消</string>
<string name="cancel_button_format">取消(%d秒</string>
<string name="total_items_format">商品數量:(%d)</string>
<string name="total_amount_format">NT$ %d</string>
<string name="cart_out_of_stock">超出庫存</string>
<string name="cart_cannot_decrease">不能減少了</string>
<string name="cart_max_items">購物車最多可購買 8 件商品</string>
<string name="cart_added">已加入購物車</string>
<string name="data_error">數據異常</string>
</resources>

View File

@ -10,6 +10,7 @@ public class SlotStructure {
private boolean lock;
private int secord;
private JSONObject otherData;
public int cartSelectedCount;// 本地欄位購物車已選數量General flavor 多品項購物車使用
/*
JSONObject jsonObject = new JSONObject();

View File

@ -716,8 +716,14 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
binding.floatButtonPickupCode.setVisibility(View.GONE);
binding.floatButtonShoppingCart.setOnClickListener(view -> {
getHandlerMain().start(getClass().getSimpleName(), Option.BUY_DIALOG_OPEN.getOption(),
getString(R.string.detect_shopping_cart));
// 改由 flavor SaleFlowHandler 決定General 開多品項購物車 Fragment
// 其他 flavor 維持原行為onShoppingCartRequested 預設轉呼叫 onBuyRequested
if (saleFlowHandler != null) {
saleFlowHandler.onShoppingCartRequested();
} else {
getHandlerMain().start(getClass().getSimpleName(), Option.BUY_DIALOG_OPEN.getOption(),
getString(R.string.detect_shopping_cart));
}
});
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
@ -1135,6 +1141,28 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
}
/**
* 將指定 Fragment 加入容器若尚未加入並切換顯示
* flavor 端注入專屬 Fragment General 多品項購物車使用本身不依賴任何 flavor 類別
*/
public void openFragment(Fragment to) {
if (to == null) {
return;
}
if (!to.isAdded()) {
getSupportFragmentManager().beginTransaction()
.add(binding.fragmentContainer.getId(), to)
.hide(to)
.commitNow();
}
switchFragment(activeFragment, to);
}
/** 切回 VMC 商品列表(供購物車「加購/取消/逾時」返回使用)。 */
public void backToVmc() {
switchFragment(activeFragment, vmcFragment);
}
public void callbackOnBackPressed() {
FragmentManager fragmentManager = getSupportFragmentManager();
int backStackCount = fragmentManager.getBackStackEntryCount();

View File

@ -177,6 +177,16 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
// 查詢失敗時不阻擋讓原流程繼續
}
// flavor 路由若該 flavor 自行消化此次點擊 General 多品項購物車加入購物車
// 則直接結束否則走以下既有流程晟崴員工卡取貨等行為不變
if (fragment.getActivity() instanceof FontendActivity) {
com.unibuy.smartdevice.ui.SaleFlowHandler handler =
((FontendActivity) fragment.getActivity()).getSaleFlowHandler();
if (handler != null && handler.handleProductSelected(slotProduct)) {
return;
}
}
// 點選時即時檢查出貨口狀態上鎖防止重複點擊
isCheckingSlot = true;
checkSlotHaveProduct(slotProduct.getSlot(), 1, () -> {

View File

@ -1,4 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}

View File

@ -17,6 +17,11 @@ media3-ui = '1.5.1'
workRuntime = "2.10.0"
tsnackBar = "V2.0.0"
# Kotlin / MVVM為 General flavor 的多品項購物車)
kotlin = "1.9.24"
lifecycle = "2.6.2"
brvah = "3.0.14"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@ -35,6 +40,14 @@ media3-ui = {group = "androidx.media3", name = "media3-ui", version.ref = "media
work-runtime = { group = "androidx.work", name = "work-runtime", version.ref = "workRuntime" }
tsnackBar = { group = "com.github.Redman1037", name = "TSnackBar", version.ref = "tsnackBar" }
# Kotlin / MVVM為 General flavor 的多品項購物車)
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
brvah = { group = "io.github.cymchad", name = "BaseRecyclerViewAdapterHelper", version.ref = "brvah" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }