完成店铺创建

This commit is contained in:
2026-03-13 16:32:37 +08:00
parent 3617a6a086
commit b180aeabd8
7 changed files with 1713 additions and 40 deletions

View File

@@ -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 || '上传异常')

View File

@@ -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>