完成店铺创建
This commit is contained in:
@@ -18,6 +18,10 @@ import PlaceholderPage from '@/layouts/admin/components/PlaceholderPage.uvue'
|
|||||||
import HomeIndex from '@/layouts/admin/pages/HomeIndex.uvue'
|
import HomeIndex from '@/layouts/admin/pages/HomeIndex.uvue'
|
||||||
import UserCenter from '@/pages/mall/admin/userCenter/index.uvue'
|
import UserCenter from '@/pages/mall/admin/userCenter/index.uvue'
|
||||||
|
|
||||||
|
// --- 店铺模块 ---
|
||||||
|
import ShopManage from '@/pages/mall/admin/shop/manage.uvue'
|
||||||
|
import ShopCreate from '@/pages/mall/admin/shop/create.uvue'
|
||||||
|
|
||||||
// --- 用户模块 ---
|
// --- 用户模块 ---
|
||||||
import UserStatistic from '@/pages/mall/admin/user/statistics/index.uvue'
|
import UserStatistic from '@/pages/mall/admin/user/statistics/index.uvue'
|
||||||
import UserList from '@/pages/mall/admin/user/management/index.uvue'
|
import UserList from '@/pages/mall/admin/user/management/index.uvue'
|
||||||
@@ -180,8 +184,12 @@ import MaintainSysInfo from '@/pages/mall/admin/maintain/sys/info.uvue'
|
|||||||
export const componentMap: Map<string, any> = new Map([
|
export const componentMap: Map<string, any> = new Map([
|
||||||
// 首页
|
// 首页
|
||||||
['HomeIndex', HomeIndex],
|
['HomeIndex', HomeIndex],
|
||||||
['UserCenter', UserCenter],
|
['UserCenter', UserCenter],
|
||||||
|
|
||||||
|
// 店铺模块
|
||||||
|
['ShopManage', ShopManage],
|
||||||
|
['ShopCreate', ShopCreate],
|
||||||
|
|
||||||
// 用户模块
|
// 用户模块
|
||||||
['UserStatistic', UserStatistic],
|
['UserStatistic', UserStatistic],
|
||||||
['UserList', UserList],
|
['UserList', UserList],
|
||||||
|
|||||||
@@ -67,12 +67,22 @@ export const topMenus: TopMenu[] = [
|
|||||||
order: 1,
|
order: 1,
|
||||||
groups: []
|
groups: []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'shop',
|
||||||
|
title: '店铺',
|
||||||
|
icon: 'shop',
|
||||||
|
path: '/pages/mall/admin/shop/manage',
|
||||||
|
order: 2,
|
||||||
|
groups: [
|
||||||
|
{ id: 'shop-manage', title: '', order: 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'user',
|
id: 'user',
|
||||||
title: '用户',
|
title: '用户',
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
path: '/pages/mall/admin/user/management/index',
|
path: '/pages/mall/admin/user/management/index',
|
||||||
order: 2,
|
order: 3,
|
||||||
groups: [
|
groups: [
|
||||||
{ id: 'user-manage', title: '', order: 1 }
|
{ id: 'user-manage', title: '', order: 1 }
|
||||||
]
|
]
|
||||||
@@ -233,6 +243,27 @@ export const routes: RouteRecord[] = [
|
|||||||
order: 1
|
order: 1
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ========== 店铺模块 ==========
|
||||||
|
{
|
||||||
|
id: 'shop_manage',
|
||||||
|
title: '店铺管理',
|
||||||
|
path: '/pages/mall/admin/shop/manage',
|
||||||
|
componentKey: 'ShopManage',
|
||||||
|
parentId: 'shop',
|
||||||
|
groupId: 'shop-manage',
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shop_create',
|
||||||
|
title: '申请入驻',
|
||||||
|
path: '/pages/mall/admin/shop/create',
|
||||||
|
componentKey: 'ShopCreate',
|
||||||
|
parentId: 'shop',
|
||||||
|
groupId: 'shop-manage',
|
||||||
|
hidden: true,
|
||||||
|
order: 2
|
||||||
|
},
|
||||||
|
|
||||||
// ========== 用户模块 ==========
|
// ========== 用户模块 ==========
|
||||||
// ========== 个人中心 ==========
|
// ========== 个人中心 ==========
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -98,11 +98,11 @@ export async function refreshAdminRole(): Promise<string> {
|
|||||||
export function getVisibleTopMenuIds(role: string): string[] {
|
export function getVisibleTopMenuIds(role: string): string[] {
|
||||||
const normRole = normalizeRole(role)
|
const normRole = normalizeRole(role)
|
||||||
if (normRole === 'admin') {
|
if (normRole === 'admin') {
|
||||||
return ['home', 'user', 'order', 'product', 'marketing', 'distribution', 'kefu', 'finance', 'cms', 'decoration', 'app', 'setting', 'maintain']
|
return ['home', 'shop', 'user', 'order', 'product', 'marketing', 'distribution', 'kefu', 'finance', 'cms', 'decoration', 'app', 'setting', 'maintain']
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normRole === 'merchant') {
|
if (normRole === 'merchant') {
|
||||||
return ['home', 'order', 'product', 'marketing', 'finance']
|
return ['home', 'shop', 'order', 'product', 'marketing', 'finance']
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['home']
|
return ['home']
|
||||||
@@ -123,7 +123,7 @@ export function hasAdminModuleAccess(moduleId: string | undefined): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (normRole === 'merchant') {
|
if (normRole === 'merchant') {
|
||||||
const allowed = ['home', 'order', 'product', 'marketing', 'finance']
|
const allowed = ['home', 'shop', 'order', 'product', 'marketing', 'finance']
|
||||||
return allowed.includes(moduleId)
|
return allowed.includes(moduleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,10 +160,19 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance'
|
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance'
|
||||||
|
import { SUPA_URL } from '@/ak/config.uts'
|
||||||
|
|
||||||
const activeStep = ref(0)
|
const activeStep = ref(0)
|
||||||
const steps = ['基础信息', '规格库存', '商品详情', '物流设置', '会员价/佣金', '营销设置', '其他设置']
|
const steps = ['基础信息', '规格库存', '商品详情', '物流设置', '会员价/佣金', '营销设置', '其他设置']
|
||||||
|
|
||||||
|
interface CategoryOption {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
const categoryOptions = ref<CategoryOption[]>([])
|
||||||
|
const categories = ref<string[]>([])
|
||||||
|
const categoryName = ref('')
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
id: '',
|
id: '',
|
||||||
merchant_id: '',
|
merchant_id: '',
|
||||||
@@ -181,9 +190,6 @@ const formData = ref({
|
|||||||
published_at: null as string | null
|
published_at: null as string | null
|
||||||
})
|
})
|
||||||
|
|
||||||
const categories = ref(['361度', '特步', '匹克', '生活家居'])
|
|
||||||
const categoryName = ref('')
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await ensureSupabaseReady()
|
await ensureSupabaseReady()
|
||||||
const mId = supa.getSession().user?.id as string | null
|
const mId = supa.getSession().user?.id as string | null
|
||||||
@@ -193,6 +199,9 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
formData.value.merchant_id = mId
|
formData.value.merchant_id = mId
|
||||||
|
|
||||||
|
// 加载真实分类
|
||||||
|
await loadCategoryOptions()
|
||||||
|
|
||||||
const editId = uni.getStorageSync('edit_product_id') as string | null
|
const editId = uni.getStorageSync('edit_product_id') as string | null
|
||||||
if (editId) {
|
if (editId) {
|
||||||
uni.removeStorageSync('edit_product_id')
|
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) {
|
async function fetchProductDetail(id: string, mId: string) {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supa
|
const { data, error } = await supa
|
||||||
@@ -228,8 +258,11 @@ async function fetchProductDetail(id: string, mId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to map category
|
// Try to map category
|
||||||
formData.value.category_id = data.category_id || ''
|
formData.value.category_id = data.category_id as string || ''
|
||||||
categoryName.value = data.category_id ? '已绑定分类' : ''
|
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) {
|
} catch (e) {
|
||||||
console.error('获取详情失败', e)
|
console.error('获取详情失败', e)
|
||||||
@@ -239,9 +272,9 @@ async function fetchProductDetail(id: string, mId: string) {
|
|||||||
|
|
||||||
function onCategoryChange(e: any) {
|
function onCategoryChange(e: any) {
|
||||||
const v = e.detail.value as number
|
const v = e.detail.value as number
|
||||||
categoryName.value = categories.value[v]
|
const selected = categoryOptions.value[v]
|
||||||
// In a real project, this maps to an actual category ID, but for now we use a mock one
|
categoryName.value = selected.name
|
||||||
formData.value.category_id = `cat_${v}`
|
formData.value.category_id = selected.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTag() {
|
function addTag() {
|
||||||
@@ -305,16 +338,13 @@ async function uploadToSupabase(filePath: string): Promise<string> {
|
|||||||
|
|
||||||
uni.showLoading({ title: '上传中...' })
|
uni.showLoading({ title: '上传中...' })
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supa.storage.from('zhipao').upload(remotePath, filePath, {})
|
const res = await supa.storage.from('zhipao').upload(remotePath, filePath, {})
|
||||||
if (error) {
|
if (res.error != null) {
|
||||||
throw error
|
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
|
return `${SUPA_URL}/storage/v1/object/public/zhipao/${remotePath}`
|
||||||
if (urlKey) {
|
|
||||||
return `https://ak3.oulog.com/storage/v1/object/public/${urlKey}`
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('上传文件失败:', e)
|
console.error('上传文件失败:', e)
|
||||||
throw new Error(e.message || '上传异常')
|
throw new Error(e.message || '上传异常')
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="product-list-page">
|
<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. 搜索表单 -->
|
<!-- 1. 搜索表单 -->
|
||||||
<view class="search-card">
|
<view class="search-card">
|
||||||
<view class="search-row">
|
<view class="search-row">
|
||||||
@@ -158,15 +172,19 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<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 { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||||
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
||||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance'
|
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance'
|
||||||
|
|
||||||
|
const hasShop = ref(false)
|
||||||
|
const shopLoading = ref(true)
|
||||||
|
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const activeStatus = ref('selling')
|
const activeStatus = ref('selling')
|
||||||
const activeDropdownId = ref<number | null>(null)
|
const activeDropdownId = ref<number | null>(null)
|
||||||
@@ -181,13 +199,64 @@ const statusTabs = ref([
|
|||||||
|
|
||||||
const productList = ref<any[]>([])
|
const productList = ref<any[]>([])
|
||||||
|
|
||||||
onMounted(() => {
|
// 监听 activeStatus 变化
|
||||||
|
watch(activeStatus, () => {
|
||||||
fetchProducts()
|
fetchProducts()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 商品模块店铺门禁
|
||||||
|
onMounted(async () => {
|
||||||
|
await checkShop()
|
||||||
uni.$on('REFRESH_PRODUCT_LIST', () => {
|
uni.$on('REFRESH_PRODUCT_LIST', () => {
|
||||||
fetchProducts()
|
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(() => {
|
onUnmounted(() => {
|
||||||
uni.$off('REFRESH_PRODUCT_LIST')
|
uni.$off('REFRESH_PRODUCT_LIST')
|
||||||
})
|
})
|
||||||
@@ -196,8 +265,8 @@ onUnmounted(() => {
|
|||||||
async function fetchProducts() {
|
async function fetchProducts() {
|
||||||
await ensureSupabaseReady()
|
await ensureSupabaseReady()
|
||||||
|
|
||||||
// 从本地缓存获取 current merchant_id
|
// merchant_id 来自 ml_shops 所关联的 ak_users.id(即 auth user id)
|
||||||
const currentMerchantId = supa.getSession().user?.id as string | null
|
const currentMerchantId = supa.getSession().user?.getString('id')
|
||||||
|
|
||||||
if (!currentMerchantId) {
|
if (!currentMerchantId) {
|
||||||
uni.showToast({ title: '未获取到商家信息,请重新登录', icon: 'none' })
|
uni.showToast({ title: '未获取到商家信息,请重新登录', icon: 'none' })
|
||||||
@@ -205,37 +274,62 @@ async function fetchProducts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data, error, count } = await supa
|
const query = supa
|
||||||
.from('ml_products')
|
.from('ml_products')
|
||||||
.select('id, name, main_image_url, base_price, available_stock, status, created_at', { count: 'exact' })
|
.select('id, name, main_image_url, base_price, available_stock, status, created_at', { count: 'exact' })
|
||||||
.eq('merchant_id', currentMerchantId)
|
.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) {
|
if (error) {
|
||||||
console.error('Fetch products error:', error)
|
console.error('Fetch products error:', error)
|
||||||
|
uni.showToast({ title: '加载失败: ' + error.message, icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data != null) {
|
||||||
productList.value = data.map((item: any) => {
|
const dataArray = data as Array<UTSJSONObject>
|
||||||
|
productList.value = dataArray.map((item: UTSJSONObject): any => {
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.get('id'),
|
||||||
image: item.main_image_url || '',
|
image: item.get('main_image_url') || '',
|
||||||
name: item.name || '未命名商品',
|
name: item.get('name') || '未命名商品',
|
||||||
activities: [],
|
activities: [],
|
||||||
typeName: '普通商品',
|
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,
|
sales: 0,
|
||||||
stock: item.available_stock || 0,
|
stock: item.get('available_stock') || 0,
|
||||||
sort: 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)
|
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;
|
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 {
|
.search-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: var(--admin-card-padding);
|
padding: var(--admin-card-padding);
|
||||||
@@ -590,3 +719,5 @@ function moveToRecycle(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
613
pages/mall/admin/shop/create.uvue
Normal file
613
pages/mall/admin/shop/create.uvue
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
<!-- 创建店铺页(入驻申请) -->
|
||||||
|
<template>
|
||||||
|
<view class="shop-create-page">
|
||||||
|
<view class="page-header">
|
||||||
|
<text class="page-title">申请入驻</text>
|
||||||
|
<text class="page-subtitle">填写以下信息提交店铺申请</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 店铺类型 -->
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="card-title"><text class="required">*</text><text>店铺类型</text></view>
|
||||||
|
<view class="shop-type-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in shopTypes"
|
||||||
|
:key="index"
|
||||||
|
class="shop-type-item"
|
||||||
|
:class="{ selected: shopType === item.value }"
|
||||||
|
@click="shopType = item.value"
|
||||||
|
>
|
||||||
|
<text class="type-icon">{{ item.icon }}</text>
|
||||||
|
<text class="type-name">{{ item.label }}</text>
|
||||||
|
<text class="type-desc">{{ item.desc }}</text>
|
||||||
|
<view v-if="shopType === item.value" class="check-mark"><text>✓</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 基础信息 -->
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="card-title">基础信息</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label"><text class="required">*</text><text>店铺名称</text></view>
|
||||||
|
<input class="form-input" v-model="shopName" placeholder="请输入店铺名称(2-50字)" maxlength="50" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">店铺 Logo</view>
|
||||||
|
<view class="upload-area" @click="pickLogoImage">
|
||||||
|
<image v-if="shopLogoLocal" :src="shopLogoLocal" class="preview-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传 Logo</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="form-tip">建议尺寸 200×200,支持 jpg/png,不超过 2MB</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">店铺简介</view>
|
||||||
|
<textarea
|
||||||
|
class="form-textarea"
|
||||||
|
v-model="description"
|
||||||
|
placeholder="请简要介绍店铺(不超过200字)"
|
||||||
|
maxlength="200"
|
||||||
|
></textarea>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 联系方式 -->
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="card-title">联系方式</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">联系人</view>
|
||||||
|
<input class="form-input" v-model="contactName" placeholder="请输入联系人姓名" maxlength="50" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">联系电话</view>
|
||||||
|
<input class="form-input" v-model="contactPhone" placeholder="请输入手机号" maxlength="20" type="number" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 资质信息 -->
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="card-title">资质信息</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">营业执照号</view>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
v-model="businessLicense"
|
||||||
|
placeholder="请输入营业执照注册号(个人店可不填)"
|
||||||
|
maxlength="50"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">身份证正面</view>
|
||||||
|
<view class="upload-area" @click="pickIdCardFront">
|
||||||
|
<image v-if="idCardFrontLocal" :src="idCardFrontLocal" class="preview-img cert-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传身份证正面</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">身份证反面</view>
|
||||||
|
<view class="upload-area" @click="pickIdCardBack">
|
||||||
|
<image v-if="idCardBackLocal" :src="idCardBackLocal" class="preview-img cert-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传身份证反面</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="form-item"
|
||||||
|
v-if="shopType !== 'personal'"
|
||||||
|
>
|
||||||
|
<view class="form-label">营业执照图片</view>
|
||||||
|
<view class="upload-area" @click="pickLicenseImage">
|
||||||
|
<image v-if="licenseImageLocal" :src="licenseImageLocal" class="preview-img cert-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传营业执照</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="form-item"
|
||||||
|
v-if="shopType === 'flagship' || shopType === 'exclusive' || shopType === 'specialty'"
|
||||||
|
>
|
||||||
|
<view class="form-label">品牌授权书</view>
|
||||||
|
<view class="upload-area" @click="pickBrandAuth">
|
||||||
|
<image v-if="brandAuthLocal" :src="brandAuthLocal" class="preview-img cert-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传品牌授权书</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="form-tip">旗舰店/专卖店/专营店必须提供品牌授权书</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 协议勾选 -->
|
||||||
|
<view class="form-card agreement-card">
|
||||||
|
<view class="agreement-row" @click="agreed = !agreed">
|
||||||
|
<view class="checkbox" :class="{ checked: agreed }">
|
||||||
|
<text v-if="agreed" class="check-icon">✓</text>
|
||||||
|
</view>
|
||||||
|
<text class="agreement-text">
|
||||||
|
我已阅读并同意
|
||||||
|
<text class="agreement-link">《平台合作协议》</text>
|
||||||
|
和
|
||||||
|
<text class="agreement-link">《商家入驻规范》</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="submit-area">
|
||||||
|
<button
|
||||||
|
class="btn-submit"
|
||||||
|
:class="{ disabled: submitting }"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
<text>{{ submitting ? '提交中...' : '提交申请' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import { SUPA_URL } from '@/ak/config.uts'
|
||||||
|
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||||
|
|
||||||
|
// ===== 表单字段 =====
|
||||||
|
const shopType = ref('personal')
|
||||||
|
const shopName = ref('')
|
||||||
|
const shopLogoLocal = ref('')
|
||||||
|
const description = ref('')
|
||||||
|
const contactName = ref('')
|
||||||
|
const contactPhone = ref('')
|
||||||
|
const businessLicense = ref('')
|
||||||
|
const idCardFrontLocal = ref('')
|
||||||
|
const idCardBackLocal = ref('')
|
||||||
|
const licenseImageLocal = ref('')
|
||||||
|
const brandAuthLocal = ref('')
|
||||||
|
const agreed = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// ===== 店铺类型配置 =====
|
||||||
|
const shopTypes = [
|
||||||
|
{ value: 'personal', label: '个人店', icon: '👤', desc: '个人经营,无需工商注册' },
|
||||||
|
{ value: 'individual', label: '个体工商户', icon: '🏪', desc: '持有个体工商营业执照' },
|
||||||
|
{ value: 'enterprise', label: '普通企业店', icon: '🏢', desc: '持有企业营业执照' },
|
||||||
|
{ value: 'flagship', label: '旗舰店', icon: '🏆', desc: '品牌官方旗舰,需品牌授权' },
|
||||||
|
{ value: 'exclusive', label: '专卖店', icon: '⭐', desc: '单一品牌授权经营' },
|
||||||
|
{ value: 'specialty', label: '专营店', icon: '🎯', desc: '多品牌品类授权经营' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ===== 图片选择 =====
|
||||||
|
function pickLogoImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) shopLogoLocal.value = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickIdCardFront() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) idCardFrontLocal.value = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickIdCardBack() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) idCardBackLocal.value = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickLicenseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) licenseImageLocal.value = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBrandAuth() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) brandAuthLocal.value = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 上传单张图片到 Supabase storage =====
|
||||||
|
async function uploadImage(localPath: string, folder: string): Promise<string> {
|
||||||
|
if (!localPath) return ''
|
||||||
|
if (localPath.startsWith('http') && !localPath.startsWith('blob:')) return localPath
|
||||||
|
|
||||||
|
const extMatch = localPath.match(/\.(\w+)$/)
|
||||||
|
const ext = extMatch ? extMatch[1] : 'jpg'
|
||||||
|
const uuid = Math.random().toString(36).substring(2, 10)
|
||||||
|
const remotePath = `shop/${folder}_${Date.now()}_${uuid}.${ext}`
|
||||||
|
|
||||||
|
const res = await supa.storage.from('zhipao').upload(remotePath, localPath, {})
|
||||||
|
if (res.error != null) throw new Error('图片上传失败')
|
||||||
|
|
||||||
|
return `${SUPA_URL}/storage/v1/object/public/zhipao/${remotePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 表单校验 =====
|
||||||
|
function validate(): boolean {
|
||||||
|
if (!shopName.value.trim()) {
|
||||||
|
uni.showToast({ title: '请输入店铺名称', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (shopName.value.trim().length < 2) {
|
||||||
|
uni.showToast({ title: '店铺名称至少2个字符', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!agreed.value) {
|
||||||
|
uni.showToast({ title: '请阅读并同意平台协议', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 提交 =====
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!validate()) return
|
||||||
|
|
||||||
|
const userId = supa.getSession().user?.getString('id')
|
||||||
|
if (!userId) {
|
||||||
|
uni.showToast({ title: '未获取到登录信息,请重新登录', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
uni.showLoading({ title: '提交中...' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 上传所有图片,拿到正式 URL
|
||||||
|
const [shopLogoUrl, idCardFrontUrl, idCardBackUrl, licenseImageUrl, brandAuthUrl] =
|
||||||
|
await Promise.all([
|
||||||
|
shopLogoLocal.value ? uploadImage(shopLogoLocal.value, 'logo') : Promise.resolve(''),
|
||||||
|
idCardFrontLocal.value ? uploadImage(idCardFrontLocal.value, 'id_front') : Promise.resolve(''),
|
||||||
|
idCardBackLocal.value ? uploadImage(idCardBackLocal.value, 'id_back') : Promise.resolve(''),
|
||||||
|
licenseImageLocal.value ? uploadImage(licenseImageLocal.value, 'license') : Promise.resolve(''),
|
||||||
|
brandAuthLocal.value ? uploadImage(brandAuthLocal.value, 'brand_auth') : Promise.resolve(''),
|
||||||
|
])
|
||||||
|
|
||||||
|
// 2. 写入 ml_shops(仅写表中真实存在的字段)
|
||||||
|
const shopPayload = {
|
||||||
|
merchant_id: userId,
|
||||||
|
shop_name: shopName.value.trim(),
|
||||||
|
status: 1
|
||||||
|
} as UTSJSONObject
|
||||||
|
if (shopLogoUrl) shopPayload['shop_logo'] = shopLogoUrl
|
||||||
|
if (description.value.trim()) shopPayload['description'] = description.value.trim()
|
||||||
|
if (businessLicense.value.trim()) shopPayload['business_license'] = businessLicense.value.trim()
|
||||||
|
if (contactName.value.trim()) shopPayload['contact_name'] = contactName.value.trim()
|
||||||
|
if (contactPhone.value.trim()) shopPayload['contact_phone'] = contactPhone.value.trim()
|
||||||
|
|
||||||
|
const shopRes = await supa.from('ml_shops').insert(shopPayload).execute()
|
||||||
|
|
||||||
|
if (shopRes.error != null) {
|
||||||
|
console.error('[ShopCreate] 创建店铺失败:', shopRes.error)
|
||||||
|
const errMsg = shopRes.error?.getString ? shopRes.error.getString('message') ?? '创建失败' : '创建失败'
|
||||||
|
uni.showToast({ title: '创建失败:' + errMsg, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shopRes.data) {
|
||||||
|
uni.showToast({ title: '未收到服务器返回数据,请重试', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 确认拿到 merchant_id(来自 ml_shops 返回)
|
||||||
|
const dataArr = shopRes.data as Array<UTSJSONObject>
|
||||||
|
let createdMerchantId = ''
|
||||||
|
if (dataArr.length > 0) {
|
||||||
|
const row = dataArr[0] as UTSJSONObject
|
||||||
|
createdMerchantId = row.getString('merchant_id') ?? userId
|
||||||
|
console.log('[ShopCreate] 店铺创建成功,merchant_id:', createdMerchantId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 写入扩展资质到 ml_user_profiles.verification_data(JSONB)
|
||||||
|
const extData = {
|
||||||
|
shop_type: shopType.value,
|
||||||
|
id_card_front: idCardFrontUrl,
|
||||||
|
id_card_back: idCardBackUrl,
|
||||||
|
license_image: licenseImageUrl,
|
||||||
|
brand_authorization: brandAuthUrl,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
} as UTSJSONObject
|
||||||
|
|
||||||
|
// 先查询是否已存在该用户的 profile
|
||||||
|
const existRes = await supa.from('ml_user_profiles')
|
||||||
|
.select('id')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single()
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
const profileExists = existRes.data != null && !existRes.error
|
||||||
|
|
||||||
|
if (profileExists) {
|
||||||
|
await supa.from('ml_user_profiles')
|
||||||
|
.update({ verification_data: extData } as UTSJSONObject)
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.execute()
|
||||||
|
} else {
|
||||||
|
await supa.from('ml_user_profiles')
|
||||||
|
.insert({ user_id: userId, verification_data: extData } as UTSJSONObject)
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({ title: '申请已提交', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
openRoute('shop_manage')
|
||||||
|
}, 1200)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[ShopCreate] 异常:', e)
|
||||||
|
uni.showToast({ title: e?.message ?? '提交失败,请重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.shop-create-page {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.page-subtitle {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 店铺类型网格 */
|
||||||
|
.shop-type-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.shop-type-item {
|
||||||
|
position: relative;
|
||||||
|
width: 140px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 14px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.shop-type-item.selected {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #e6f4ff;
|
||||||
|
}
|
||||||
|
.type-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.type-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.type-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.check-mark {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #1890ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text { font-size: 11px; color: #fff; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单项 */
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
&:last-child { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 90px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
.form-tip {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #aaa;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.upload-area {
|
||||||
|
width: 120px;
|
||||||
|
height: 90px;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
&:hover { border-color: #1890ff; }
|
||||||
|
}
|
||||||
|
.upload-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
.upload-txt {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
.preview-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.cert-img {
|
||||||
|
width: 200px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 协议 */
|
||||||
|
.agreement-card { padding: 16px 24px; }
|
||||||
|
.agreement-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.checkbox {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.checkbox.checked {
|
||||||
|
background: #1890ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
.check-icon { font-size: 11px; color: #fff; }
|
||||||
|
.agreement-text { font-size: 13px; color: #606266; }
|
||||||
|
.agreement-link { color: #1890ff; }
|
||||||
|
|
||||||
|
/* 提交 */
|
||||||
|
.submit-area {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
.btn-submit {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.btn-submit.disabled {
|
||||||
|
background: #a0cfff;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
860
pages/mall/admin/shop/manage.uvue
Normal file
860
pages/mall/admin/shop/manage.uvue
Normal file
@@ -0,0 +1,860 @@
|
|||||||
|
<!-- 店铺管理页:查看 / 编辑店铺信息 -->
|
||||||
|
<template>
|
||||||
|
<view class="shop-manage-page">
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<view v-if="pageLoading" class="loading-wrap">
|
||||||
|
<text class="loading-txt">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 无店铺:引导空态 -->
|
||||||
|
<template v-else-if="!hasShop">
|
||||||
|
<view class="empty-wrap">
|
||||||
|
<text class="empty-icon">🏪</text>
|
||||||
|
<text class="empty-title">您还没有创建店铺</text>
|
||||||
|
<text class="empty-desc">注册店铺后即可发布商品、管理订单</text>
|
||||||
|
<button class="btn-create" @click="goToCreate">立即创建店铺</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 有店铺:查看 / 编辑 -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- 标题行 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<text class="page-title">店铺管理</text>
|
||||||
|
<view class="header-actions">
|
||||||
|
<button v-if="!isEditing" class="btn-edit" @click="enterEdit">编辑信息</button>
|
||||||
|
<view v-else class="edit-btns">
|
||||||
|
<button class="btn-cancel" @click="cancelEdit">取消</button>
|
||||||
|
<button class="btn-save" :class="{ disabled: saving }" :disabled="saving" @click="saveShop">
|
||||||
|
{{ saving ? '保存中...' : '保存' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 店铺基础信息卡 -->
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="card-title">基础信息</view>
|
||||||
|
|
||||||
|
<!-- Logo -->
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">店铺 Logo</view>
|
||||||
|
<view v-if="!isEditing" class="view-mode">
|
||||||
|
<image
|
||||||
|
v-if="form.shop_logo"
|
||||||
|
:src="form.shop_logo"
|
||||||
|
class="preview-logo"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<text v-else class="empty-val">未设置</text>
|
||||||
|
</view>
|
||||||
|
<view v-else>
|
||||||
|
<view class="upload-area" @click="pickLogoImage">
|
||||||
|
<image v-if="form.shop_logo" :src="form.shop_logo" class="preview-logo upload-preview" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传 Logo</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 店铺名称 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label"><text class="required" v-if="isEditing">*</text><text>店铺名称</text></view>
|
||||||
|
<text v-if="!isEditing" class="view-val">{{ form.shop_name || '-' }}</text>
|
||||||
|
<input v-else class="form-input" v-model="form.shop_name" placeholder="请输入店铺名称" maxlength="50" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 店铺简介 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">店铺简介</view>
|
||||||
|
<text v-if="!isEditing" class="view-val multiline">{{ form.description || '-' }}</text>
|
||||||
|
<textarea v-else class="form-textarea" v-model="form.description" placeholder="请输入店铺简介" maxlength="200"></textarea>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 联系方式卡 -->
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="card-title">联系方式</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">联系人</view>
|
||||||
|
<text v-if="!isEditing" class="view-val">{{ form.contact_name || '-' }}</text>
|
||||||
|
<input v-else class="form-input" v-model="form.contact_name" placeholder="请输入联系人" maxlength="50" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">联系电话</view>
|
||||||
|
<text v-if="!isEditing" class="view-val">{{ form.contact_phone || '-' }}</text>
|
||||||
|
<input v-else class="form-input" v-model="form.contact_phone" placeholder="请输入联系电话" maxlength="20" type="number" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">联系邮箱</view>
|
||||||
|
<text v-if="!isEditing" class="view-val">{{ form.contact_email || '-' }}</text>
|
||||||
|
<input v-else class="form-input" v-model="form.contact_email" placeholder="选填" maxlength="100" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 资质信息卡 -->
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="card-title">资质信息</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">营业执照号</view>
|
||||||
|
<text v-if="!isEditing" class="view-val">{{ form.business_license || '-' }}</text>
|
||||||
|
<input v-else class="form-input" v-model="form.business_license" placeholder="营业执照注册号" maxlength="50" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 扩展资质(存在 verification_data 里) -->
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">店铺类型</view>
|
||||||
|
<text v-if="!isEditing" class="view-val">{{ getShopTypeLabel(extData.shop_type) }}</text>
|
||||||
|
<picker v-else :range="shopTypeLabels" :value="shopTypeIndex" @change="onShopTypeChange">
|
||||||
|
<view class="picker-trigger">
|
||||||
|
<text>{{ getShopTypeLabel(extData.shop_type) }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">身份证正面</view>
|
||||||
|
<view v-if="!isEditing">
|
||||||
|
<image v-if="extData.id_card_front" :src="extData.id_card_front" class="cert-img" mode="aspectFill" />
|
||||||
|
<text v-else class="empty-val">未上传</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="upload-area-wide" @click="pickIdCardFront">
|
||||||
|
<image v-if="extData.id_card_front" :src="extData.id_card_front" class="cert-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传身份证正面</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">身份证反面</view>
|
||||||
|
<view v-if="!isEditing">
|
||||||
|
<image v-if="extData.id_card_back" :src="extData.id_card_back" class="cert-img" mode="aspectFill" />
|
||||||
|
<text v-else class="empty-val">未上传</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="upload-area-wide" @click="pickIdCardBack">
|
||||||
|
<image v-if="extData.id_card_back" :src="extData.id_card_back" class="cert-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传身份证反面</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">营业执照图片</view>
|
||||||
|
<view v-if="!isEditing">
|
||||||
|
<image v-if="extData.license_image" :src="extData.license_image" class="cert-img" mode="aspectFill" />
|
||||||
|
<text v-else class="empty-val">未上传</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="upload-area-wide" @click="pickLicenseImage">
|
||||||
|
<image v-if="extData.license_image" :src="extData.license_image" class="cert-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传营业执照</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="form-label">品牌授权书</view>
|
||||||
|
<view v-if="!isEditing">
|
||||||
|
<image v-if="extData.brand_authorization" :src="extData.brand_authorization" class="cert-img" mode="aspectFill" />
|
||||||
|
<text v-else class="empty-val">未上传</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="upload-area-wide" @click="pickBrandAuth">
|
||||||
|
<image v-if="extData.brand_authorization" :src="extData.brand_authorization" class="cert-img" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<text class="upload-icon">+</text>
|
||||||
|
<text class="upload-txt">上传品牌授权书</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 店铺状态信息(只读) -->
|
||||||
|
<view class="form-card info-card">
|
||||||
|
<view class="card-title">店铺状态</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">店铺ID</text>
|
||||||
|
<text class="info-val">{{ shopId }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">商家ID</text>
|
||||||
|
<text class="info-val">{{ merchantId }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">当前状态</text>
|
||||||
|
<text class="info-val" :class="form.status === 1 ? 'status-normal' : 'status-off'">
|
||||||
|
{{ form.status === 1 ? '正常营业' : form.status === 2 ? '暂停营业' : '已关闭' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">商品数量</text>
|
||||||
|
<text class="info-val">{{ form.product_count }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">创建时间</text>
|
||||||
|
<text class="info-val">{{ formatDate(form.created_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import { SUPA_URL } from '@/ak/config.uts'
|
||||||
|
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||||
|
|
||||||
|
// ===== 页面状态 =====
|
||||||
|
const pageLoading = ref(true)
|
||||||
|
const hasShop = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
// ===== 从 ml_shops 回读的字段 =====
|
||||||
|
const shopId = ref('')
|
||||||
|
const merchantId = ref('')
|
||||||
|
|
||||||
|
// ===== 店铺表单(绑定到对应的 ml_shops 字段)=====
|
||||||
|
const form = ref({
|
||||||
|
shop_name: '',
|
||||||
|
shop_logo: '',
|
||||||
|
description: '',
|
||||||
|
contact_name: '',
|
||||||
|
contact_phone: '',
|
||||||
|
contact_email: '',
|
||||||
|
business_license: '',
|
||||||
|
status: 1,
|
||||||
|
product_count: 0,
|
||||||
|
created_at: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 编辑前的快照(用于取消时恢复)
|
||||||
|
const formSnapshot = ref({ ...form.value })
|
||||||
|
|
||||||
|
// ===== 扩展资质(存储在 ml_user_profiles.verification_data)=====
|
||||||
|
const extData = ref({
|
||||||
|
shop_type: 'personal',
|
||||||
|
id_card_front: '',
|
||||||
|
id_card_back: '',
|
||||||
|
license_image: '',
|
||||||
|
brand_authorization: ''
|
||||||
|
})
|
||||||
|
const extSnapshot = ref({ ...extData.value })
|
||||||
|
|
||||||
|
// ===== 店铺类型配置 =====
|
||||||
|
const shopTypeOptions = [
|
||||||
|
{ value: 'personal', label: '个人店' },
|
||||||
|
{ value: 'individual', label: '个体工商户' },
|
||||||
|
{ value: 'enterprise', label: '普通企业店' },
|
||||||
|
{ value: 'flagship', label: '旗舰店' },
|
||||||
|
{ value: 'exclusive', label: '专卖店' },
|
||||||
|
{ value: 'specialty', label: '专营店' },
|
||||||
|
]
|
||||||
|
const shopTypeLabels = shopTypeOptions.map(o => o.label)
|
||||||
|
const shopTypeIndex = ref(0)
|
||||||
|
|
||||||
|
function getShopTypeLabel(val: string): string {
|
||||||
|
const found = shopTypeOptions.find(o => o.value === val)
|
||||||
|
return found ? found.label : '个人店'
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShopTypeChange(e: any) {
|
||||||
|
const idx = e.detail.value as number
|
||||||
|
shopTypeIndex.value = idx
|
||||||
|
extData.value.shop_type = shopTypeOptions[idx].value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 加载当前店铺 =====
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadShop()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadShop() {
|
||||||
|
pageLoading.value = true
|
||||||
|
try {
|
||||||
|
const userId = supa.getSession().user?.getString('id')
|
||||||
|
if (!userId) {
|
||||||
|
pageLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 merchant_id 查询 ml_shops(merchant_id = ak_users.id = auth user id)
|
||||||
|
const shopRes = await supa.from('ml_shops')
|
||||||
|
.select('*')
|
||||||
|
.eq('merchant_id', userId)
|
||||||
|
.single()
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (shopRes.error != null || !shopRes.data) {
|
||||||
|
hasShop.value = false
|
||||||
|
pageLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析返回数据
|
||||||
|
const rawData = shopRes.data
|
||||||
|
let shopRow: UTSJSONObject | null = null
|
||||||
|
|
||||||
|
if (Array.isArray(rawData)) {
|
||||||
|
if ((rawData as Array<any>).length === 0) {
|
||||||
|
hasShop.value = false
|
||||||
|
pageLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shopRow = (rawData as Array<UTSJSONObject>)[0]
|
||||||
|
} else {
|
||||||
|
shopRow = rawData as UTSJSONObject
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shopRow) {
|
||||||
|
hasShop.value = false
|
||||||
|
pageLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasShop.value = true
|
||||||
|
shopId.value = shopRow.getString('id') ?? ''
|
||||||
|
merchantId.value = shopRow.getString('merchant_id') ?? userId
|
||||||
|
|
||||||
|
form.value = {
|
||||||
|
shop_name: shopRow.getString('shop_name') ?? '',
|
||||||
|
shop_logo: shopRow.getString('shop_logo') ?? '',
|
||||||
|
description: shopRow.getString('description') ?? '',
|
||||||
|
contact_name: shopRow.getString('contact_name') ?? '',
|
||||||
|
contact_phone: shopRow.getString('contact_phone') ?? '',
|
||||||
|
contact_email: shopRow.getString('contact_email') ?? '',
|
||||||
|
business_license: shopRow.getString('business_license') ?? '',
|
||||||
|
status: shopRow.getNumber('status') ?? 1,
|
||||||
|
product_count: shopRow.getNumber('product_count') ?? 0,
|
||||||
|
created_at: shopRow.getString('created_at') ?? ''
|
||||||
|
}
|
||||||
|
formSnapshot.value = { ...form.value }
|
||||||
|
|
||||||
|
// 加载扩展资质
|
||||||
|
await loadExtData(userId)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[ShopManage] 加载店铺失败:', e)
|
||||||
|
hasShop.value = false
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadExtData(userId: string) {
|
||||||
|
try {
|
||||||
|
const profileRes = await supa.from('ml_user_profiles')
|
||||||
|
.select('verification_data')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single()
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (profileRes.error != null || !profileRes.data) return
|
||||||
|
|
||||||
|
const rawData = profileRes.data
|
||||||
|
let profileRow: UTSJSONObject | null = null
|
||||||
|
if (Array.isArray(rawData)) {
|
||||||
|
if ((rawData as Array<any>).length > 0) {
|
||||||
|
profileRow = (rawData as Array<UTSJSONObject>)[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
profileRow = rawData as UTSJSONObject
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profileRow) return
|
||||||
|
|
||||||
|
const vd = profileRow.getJSON('verification_data')
|
||||||
|
if (vd != null) {
|
||||||
|
extData.value = {
|
||||||
|
shop_type: vd.getString('shop_type') ?? 'personal',
|
||||||
|
id_card_front: vd.getString('id_card_front') ?? '',
|
||||||
|
id_card_back: vd.getString('id_card_back') ?? '',
|
||||||
|
license_image: vd.getString('license_image') ?? '',
|
||||||
|
brand_authorization: vd.getString('brand_authorization') ?? ''
|
||||||
|
}
|
||||||
|
// 同步 picker index
|
||||||
|
const idx = shopTypeOptions.findIndex(o => o.value === extData.value.shop_type)
|
||||||
|
shopTypeIndex.value = idx >= 0 ? idx : 0
|
||||||
|
}
|
||||||
|
extSnapshot.value = { ...extData.value }
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn('[ShopManage] 加载扩展资质失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 编辑模式 =====
|
||||||
|
function enterEdit() {
|
||||||
|
formSnapshot.value = { ...form.value }
|
||||||
|
extSnapshot.value = { ...extData.value }
|
||||||
|
isEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
form.value = { ...formSnapshot.value }
|
||||||
|
extData.value = { ...extSnapshot.value }
|
||||||
|
const idx = shopTypeOptions.findIndex(o => o.value === extData.value.shop_type)
|
||||||
|
shopTypeIndex.value = idx >= 0 ? idx : 0
|
||||||
|
isEditing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 图片选择 =====
|
||||||
|
function pickLogoImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) form.value.shop_logo = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickIdCardFront() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) extData.value.id_card_front = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickIdCardBack() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) extData.value.id_card_back = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickLicenseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) extData.value.license_image = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBrandAuth() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
const paths = res.tempFilePaths as string[]
|
||||||
|
if (paths.length > 0) extData.value.brand_authorization = paths[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 上传图片到 Supabase storage =====
|
||||||
|
async function uploadImage(localPath: string, folder: string): Promise<string> {
|
||||||
|
if (!localPath) return ''
|
||||||
|
if (localPath.startsWith('http') && !localPath.startsWith('blob:')) return localPath
|
||||||
|
|
||||||
|
const extMatch = localPath.match(/\.(\w+)$/)
|
||||||
|
const ext = extMatch ? extMatch[1] : 'jpg'
|
||||||
|
const uuid = Math.random().toString(36).substring(2, 10)
|
||||||
|
const remotePath = `shop/${folder}_${Date.now()}_${uuid}.${ext}`
|
||||||
|
|
||||||
|
const res = await supa.storage.from('zhipao').upload(remotePath, localPath, {})
|
||||||
|
if (res.error != null) throw new Error('图片上传失败')
|
||||||
|
|
||||||
|
return `${SUPA_URL}/storage/v1/object/public/zhipao/${remotePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 保存 =====
|
||||||
|
async function saveShop() {
|
||||||
|
if (!form.value.shop_name.trim()) {
|
||||||
|
uni.showToast({ title: '请输入店铺名称', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = supa.getSession().user?.getString('id')
|
||||||
|
if (!userId) {
|
||||||
|
uni.showToast({ title: '未获取到登录信息', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
uni.showLoading({ title: '保存中...' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 上传有变更的图片(本地路径需上传,已有 URL 直接复用)
|
||||||
|
const [logoUrl, idFrontUrl, idBackUrl, licenseUrl, brandUrl] = await Promise.all([
|
||||||
|
form.value.shop_logo ? uploadImage(form.value.shop_logo, 'logo') : Promise.resolve(''),
|
||||||
|
extData.value.id_card_front ? uploadImage(extData.value.id_card_front, 'id_front') : Promise.resolve(''),
|
||||||
|
extData.value.id_card_back ? uploadImage(extData.value.id_card_back, 'id_back') : Promise.resolve(''),
|
||||||
|
extData.value.license_image ? uploadImage(extData.value.license_image, 'license') : Promise.resolve(''),
|
||||||
|
extData.value.brand_authorization ? uploadImage(extData.value.brand_authorization, 'brand_auth') : Promise.resolve(''),
|
||||||
|
])
|
||||||
|
|
||||||
|
// 更新本地表单中的 URL(去掉临时路径)
|
||||||
|
if (logoUrl) form.value.shop_logo = logoUrl
|
||||||
|
if (idFrontUrl) extData.value.id_card_front = idFrontUrl
|
||||||
|
if (idBackUrl) extData.value.id_card_back = idBackUrl
|
||||||
|
if (licenseUrl) extData.value.license_image = licenseUrl
|
||||||
|
if (brandUrl) extData.value.brand_authorization = brandUrl
|
||||||
|
|
||||||
|
// 2. 更新 ml_shops
|
||||||
|
const shopUpdatePayload = {
|
||||||
|
shop_name: form.value.shop_name.trim(),
|
||||||
|
shop_logo: form.value.shop_logo,
|
||||||
|
description: form.value.description,
|
||||||
|
contact_name: form.value.contact_name,
|
||||||
|
contact_phone: form.value.contact_phone,
|
||||||
|
contact_email: form.value.contact_email,
|
||||||
|
business_license: form.value.business_license
|
||||||
|
} as UTSJSONObject
|
||||||
|
|
||||||
|
const updateRes = await supa.from('ml_shops')
|
||||||
|
.update(shopUpdatePayload)
|
||||||
|
.eq('merchant_id', userId)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (updateRes.error != null) {
|
||||||
|
console.error('[ShopManage] 更新 ml_shops 失败:', updateRes.error)
|
||||||
|
const errMsg = updateRes.error?.getString ? updateRes.error.getString('message') ?? '保存失败' : '保存失败'
|
||||||
|
uni.showToast({ title: '保存失败:' + errMsg, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证更新结果
|
||||||
|
if (!updateRes.data) {
|
||||||
|
console.error('[ShopManage] 更新 ml_shops 未返回数据')
|
||||||
|
} else {
|
||||||
|
console.log('[ShopManage] 更新成功,返回数据:', updateRes.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 更新 ml_user_profiles.verification_data
|
||||||
|
const extPayloadData = {
|
||||||
|
shop_type: extData.value.shop_type,
|
||||||
|
id_card_front: extData.value.id_card_front,
|
||||||
|
id_card_back: extData.value.id_card_back,
|
||||||
|
license_image: extData.value.license_image,
|
||||||
|
brand_authorization: extData.value.brand_authorization,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
} as UTSJSONObject
|
||||||
|
|
||||||
|
// 先查询是否已有对应记录
|
||||||
|
const profCheckRes = await supa.from('ml_user_profiles')
|
||||||
|
.select('id')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single()
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (profCheckRes.data != null && !profCheckRes.error) {
|
||||||
|
const profRes = await supa.from('ml_user_profiles')
|
||||||
|
.update({ verification_data: extPayloadData } as UTSJSONObject)
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.execute()
|
||||||
|
if (profRes.error != null) {
|
||||||
|
console.warn('[ShopManage] 更新扩展资质失败(不影响主信息):', profRes.error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const profRes = await supa.from('ml_user_profiles')
|
||||||
|
.insert({ user_id: userId, verification_data: extPayloadData } as UTSJSONObject)
|
||||||
|
.execute()
|
||||||
|
if (profRes.error != null) {
|
||||||
|
console.warn('[ShopManage] 写入扩展资质失败(不影响主信息):', profRes.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditing.value = false
|
||||||
|
formSnapshot.value = { ...form.value }
|
||||||
|
extSnapshot.value = { ...extData.value }
|
||||||
|
|
||||||
|
// 4. 重新拉取最新数据回显
|
||||||
|
await loadShop()
|
||||||
|
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[ShopManage] 保存异常:', e)
|
||||||
|
uni.showToast({ title: e?.message ?? '保存失败,请重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 导航 =====
|
||||||
|
function goToCreate() {
|
||||||
|
openRoute('shop_create')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 工具 =====
|
||||||
|
function formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
try {
|
||||||
|
const d = new Date(dateStr)
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||||
|
} catch (_) {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.shop-manage-page {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载 */
|
||||||
|
.loading-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
.loading-txt { font-size: 14px; color: #999; }
|
||||||
|
|
||||||
|
/* 空态 */
|
||||||
|
.empty-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 80px 40px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.empty-icon { font-size: 56px; margin-bottom: 16px; }
|
||||||
|
.empty-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 8px; }
|
||||||
|
.empty-desc { font-size: 13px; color: #999; margin-bottom: 28px; }
|
||||||
|
.btn-create {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部 */
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.btn-edit {
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 36px;
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.edit-btns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.btn-cancel {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 36px;
|
||||||
|
background: #fff;
|
||||||
|
color: #606266;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.btn-save {
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 36px;
|
||||||
|
background: #52c41a;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
&.disabled { background: #b7eb8f; cursor: not-allowed; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.required { color: #ff4d4f; margin-right: 4px; }
|
||||||
|
|
||||||
|
/* 表单项 */
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
&:last-child { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 只读模式展示 */
|
||||||
|
.view-val {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.view-val.multiline { white-space: pre-wrap; }
|
||||||
|
.empty-val { font-size: 13px; color: #aaa; }
|
||||||
|
|
||||||
|
/* Logo 预览 */
|
||||||
|
.preview-logo {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.upload-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.upload-area {
|
||||||
|
width: 100px;
|
||||||
|
height: 80px;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
&:hover { border-color: #1890ff; }
|
||||||
|
}
|
||||||
|
.upload-area-wide {
|
||||||
|
width: 200px;
|
||||||
|
height: 120px;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
&:hover { border-color: #1890ff; }
|
||||||
|
}
|
||||||
|
.upload-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.upload-icon { font-size: 20px; color: #c0c4cc; }
|
||||||
|
.upload-txt { font-size: 11px; color: #aaa; }
|
||||||
|
|
||||||
|
/* 证件图 */
|
||||||
|
.cert-img {
|
||||||
|
width: 200px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.view-mode { display: flex; flex-direction: row; align-items: center; }
|
||||||
|
|
||||||
|
/* Picker */
|
||||||
|
.picker-trigger {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 160px;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.picker-arrow { font-size: 10px; color: #c0c4cc; }
|
||||||
|
|
||||||
|
/* 状态信息 */
|
||||||
|
.info-card { }
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #f9f9f9;
|
||||||
|
&:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
|
.info-label { width: 100px; font-size: 13px; color: #999; }
|
||||||
|
.info-val { font-size: 13px; color: #333; }
|
||||||
|
.status-normal { color: #52c41a; font-weight: 500; }
|
||||||
|
.status-off { color: #ff4d4f; font-weight: 500; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user