完成店铺创建
This commit is contained in:
@@ -160,10 +160,19 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance'
|
||||
import { SUPA_URL } from '@/ak/config.uts'
|
||||
|
||||
const activeStep = ref(0)
|
||||
const steps = ['基础信息', '规格库存', '商品详情', '物流设置', '会员价/佣金', '营销设置', '其他设置']
|
||||
|
||||
interface CategoryOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
const categoryOptions = ref<CategoryOption[]>([])
|
||||
const categories = ref<string[]>([])
|
||||
const categoryName = ref('')
|
||||
|
||||
const formData = ref({
|
||||
id: '',
|
||||
merchant_id: '',
|
||||
@@ -181,9 +190,6 @@ const formData = ref({
|
||||
published_at: null as string | null
|
||||
})
|
||||
|
||||
const categories = ref(['361度', '特步', '匹克', '生活家居'])
|
||||
const categoryName = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await ensureSupabaseReady()
|
||||
const mId = supa.getSession().user?.id as string | null
|
||||
@@ -193,6 +199,9 @@ onMounted(async () => {
|
||||
}
|
||||
formData.value.merchant_id = mId
|
||||
|
||||
// 加载真实分类
|
||||
await loadCategoryOptions()
|
||||
|
||||
const editId = uni.getStorageSync('edit_product_id') as string | null
|
||||
if (editId) {
|
||||
uni.removeStorageSync('edit_product_id')
|
||||
@@ -200,6 +209,27 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
async function loadCategoryOptions() {
|
||||
try {
|
||||
const res = await supa.from('ml_categories')
|
||||
.select('id, name')
|
||||
.eq('is_active', true)
|
||||
.order('sort_order', { ascending: true })
|
||||
.execute()
|
||||
|
||||
if (res.data != null) {
|
||||
const data = res.data as Array<UTSJSONObject>
|
||||
categoryOptions.value = data.map((item: UTSJSONObject): CategoryOption => ({
|
||||
id: item.get('id') as string,
|
||||
name: item.get('name') as string
|
||||
}))
|
||||
categories.value = categoryOptions.value.map((item: CategoryOption): string => item.name)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载分类失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProductDetail(id: string, mId: string) {
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
@@ -228,8 +258,11 @@ async function fetchProductDetail(id: string, mId: string) {
|
||||
}
|
||||
|
||||
// Try to map category
|
||||
formData.value.category_id = data.category_id || ''
|
||||
categoryName.value = data.category_id ? '已绑定分类' : ''
|
||||
formData.value.category_id = data.category_id as string || ''
|
||||
if (formData.value.category_id) {
|
||||
const cat = categoryOptions.value.find((c: CategoryOption): boolean => c.id === formData.value.category_id)
|
||||
categoryName.value = cat ? cat.name : '未知分类'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取详情失败', e)
|
||||
@@ -239,9 +272,9 @@ async function fetchProductDetail(id: string, mId: string) {
|
||||
|
||||
function onCategoryChange(e: any) {
|
||||
const v = e.detail.value as number
|
||||
categoryName.value = categories.value[v]
|
||||
// In a real project, this maps to an actual category ID, but for now we use a mock one
|
||||
formData.value.category_id = `cat_${v}`
|
||||
const selected = categoryOptions.value[v]
|
||||
categoryName.value = selected.name
|
||||
formData.value.category_id = selected.id
|
||||
}
|
||||
|
||||
function addTag() {
|
||||
@@ -305,16 +338,13 @@ async function uploadToSupabase(filePath: string): Promise<string> {
|
||||
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
try {
|
||||
const { data, error } = await supa.storage.from('zhipao').upload(remotePath, filePath, {})
|
||||
if (error) {
|
||||
throw error
|
||||
const res = await supa.storage.from('zhipao').upload(remotePath, filePath, {})
|
||||
if (res.error != null) {
|
||||
throw res.error
|
||||
}
|
||||
const urlKey = typeof data === 'object' ? (data as any)['Key'] || (data as any)['path'] : ''
|
||||
// fallback logic, generally Supabase uses 'storage/v1/object/public/bucket/' + path
|
||||
if (urlKey) {
|
||||
return `https://ak3.oulog.com/storage/v1/object/public/${urlKey}`
|
||||
}
|
||||
return ''
|
||||
|
||||
return `${SUPA_URL}/storage/v1/object/public/zhipao/${remotePath}`
|
||||
|
||||
} catch (e: any) {
|
||||
console.error('上传文件失败:', e)
|
||||
throw new Error(e.message || '上传异常')
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<view class="product-list-page">
|
||||
<!-- 店铺门禁:无店铺时显示空态 -->
|
||||
<view v-if="shopLoading" class="shop-guard-loading">
|
||||
<text class="sgl-txt">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!hasShop" class="shop-guard-empty">
|
||||
<text class="sge-icon">🏦</text>
|
||||
<text class="sge-title">您还没有店铺</text>
|
||||
<text class="sge-desc">先创建店铺,才能发布商品</text>
|
||||
<button class="sge-btn" @click="goCreateShop">立即创建店铺</button>
|
||||
</view>
|
||||
|
||||
<!-- 正常商品列表 -->
|
||||
<template v-else>
|
||||
<!-- 1. 搜索表单 -->
|
||||
<view class="search-card">
|
||||
<view class="search-row">
|
||||
@@ -158,15 +172,19 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance'
|
||||
|
||||
const hasShop = ref(false)
|
||||
const shopLoading = ref(true)
|
||||
|
||||
const total = ref(0)
|
||||
const activeStatus = ref('selling')
|
||||
const activeDropdownId = ref<number | null>(null)
|
||||
@@ -181,13 +199,64 @@ const statusTabs = ref([
|
||||
|
||||
const productList = ref<any[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
// 监听 activeStatus 变化
|
||||
watch(activeStatus, () => {
|
||||
fetchProducts()
|
||||
})
|
||||
|
||||
// 商品模块店铺门禁
|
||||
onMounted(async () => {
|
||||
await checkShop()
|
||||
uni.$on('REFRESH_PRODUCT_LIST', () => {
|
||||
fetchProducts()
|
||||
})
|
||||
})
|
||||
|
||||
async function checkShop() {
|
||||
shopLoading.value = true
|
||||
try {
|
||||
await ensureSupabaseReady()
|
||||
const userId = supa.getSession().user?.getString('id')
|
||||
if (!userId) {
|
||||
hasShop.value = false
|
||||
shopLoading.value = false
|
||||
return
|
||||
}
|
||||
// 查询 ml_shops 确认当前用户是否已建店
|
||||
const res = await supa.from('ml_shops')
|
||||
.select('merchant_id, shop_name, status')
|
||||
.eq('merchant_id', userId)
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (res.error != null || !res.data) {
|
||||
hasShop.value = false
|
||||
} else {
|
||||
const rawData = res.data
|
||||
let shopRow: UTSJSONObject | null = null
|
||||
if (Array.isArray(rawData)) {
|
||||
shopRow = (rawData as Array<UTSJSONObject>).length > 0 ? (rawData as Array<UTSJSONObject>)[0] : null
|
||||
} else {
|
||||
shopRow = rawData as UTSJSONObject
|
||||
}
|
||||
hasShop.value = shopRow != null
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('[ProductList] 店铺检查异常:', e)
|
||||
hasShop.value = false
|
||||
} finally {
|
||||
shopLoading.value = false
|
||||
}
|
||||
|
||||
if (hasShop.value) {
|
||||
fetchProducts()
|
||||
}
|
||||
}
|
||||
|
||||
function goCreateShop() {
|
||||
openRoute('shop_manage')
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('REFRESH_PRODUCT_LIST')
|
||||
})
|
||||
@@ -196,8 +265,8 @@ onUnmounted(() => {
|
||||
async function fetchProducts() {
|
||||
await ensureSupabaseReady()
|
||||
|
||||
// 从本地缓存获取 current merchant_id
|
||||
const currentMerchantId = supa.getSession().user?.id as string | null
|
||||
// merchant_id 来自 ml_shops 所关联的 ak_users.id(即 auth user id)
|
||||
const currentMerchantId = supa.getSession().user?.getString('id')
|
||||
|
||||
if (!currentMerchantId) {
|
||||
uni.showToast({ title: '未获取到商家信息,请重新登录', icon: 'none' })
|
||||
@@ -205,37 +274,62 @@ async function fetchProducts() {
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error, count } = await supa
|
||||
const query = supa
|
||||
.from('ml_products')
|
||||
.select('id, name, main_image_url, base_price, available_stock, status, created_at', { count: 'exact' })
|
||||
.eq('merchant_id', currentMerchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
// 根据 activeStatus 过滤
|
||||
// 1:上架 2:下架 3:草稿 4:删除
|
||||
// selling: 1, warehouse: 2+3, soldout: stock=0, alarm: stock<10, recycle: 4
|
||||
if (activeStatus.value === 'selling') {
|
||||
query.eq('status', 1)
|
||||
} else if (activeStatus.value === 'warehouse') {
|
||||
query.in('status', [2, 3])
|
||||
} else if (activeStatus.value === 'recycle') {
|
||||
query.eq('status', 4)
|
||||
} else if (activeStatus.value === 'soldout') {
|
||||
query.eq('available_stock', 0)
|
||||
} else if (activeStatus.value === 'alarm') {
|
||||
query.lt('available_stock', 10)
|
||||
}
|
||||
|
||||
const { data, error, count } = await query.order('created_at', { ascending: false }).execute()
|
||||
|
||||
if (error) {
|
||||
console.error('Fetch products error:', error)
|
||||
uni.showToast({ title: '加载失败: ' + error.message, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (data) {
|
||||
productList.value = data.map((item: any) => {
|
||||
if (data != null) {
|
||||
const dataArray = data as Array<UTSJSONObject>
|
||||
productList.value = dataArray.map((item: UTSJSONObject): any => {
|
||||
return {
|
||||
id: item.id,
|
||||
image: item.main_image_url || '',
|
||||
name: item.name || '未命名商品',
|
||||
id: item.get('id'),
|
||||
image: item.get('main_image_url') || '',
|
||||
name: item.get('name') || '未命名商品',
|
||||
activities: [],
|
||||
typeName: '普通商品',
|
||||
price: item.base_price !== null ? Number(item.base_price).toFixed(2) : '0.00',
|
||||
price: item.get('base_price') != null ? Number(item.get('base_price')).toFixed(2) : '0.00',
|
||||
sales: 0,
|
||||
stock: item.available_stock || 0,
|
||||
stock: item.get('available_stock') || 0,
|
||||
sort: 0,
|
||||
status: item.status || 0
|
||||
status: item.get('status') || 0
|
||||
}
|
||||
})
|
||||
total.value = count || dataArray.length
|
||||
|
||||
// 更新 Tab 计数 (简单同步当前列表总数到对应 Tab)
|
||||
statusTabs.value.forEach(tab => {
|
||||
if (tab.key === activeStatus.value) {
|
||||
tab.count = total.value
|
||||
}
|
||||
})
|
||||
total.value = count || data.length
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('获取商品列表失败:', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
uni.showToast({ title: '加载失败: ' + (err.message || ''), icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +377,41 @@ function moveToRecycle(id: number) {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
/* 店铺门禁状态 */
|
||||
.shop-guard-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.sgl-txt { font-size: 14px; color: #999; }
|
||||
|
||||
.shop-guard-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80px 40px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.sge-icon { font-size: 56px; margin-bottom: 16px; }
|
||||
.sge-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 8px; }
|
||||
.sge-desc { font-size: 13px; color: #999; margin-bottom: 28px; }
|
||||
.sge-btn {
|
||||
padding: 0 32px;
|
||||
height: 40px;
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
background: #fff;
|
||||
padding: var(--admin-card-padding);
|
||||
@@ -590,3 +719,5 @@ function moveToRecycle(id: number) {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user