完成店铺创建
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 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 UserList from '@/pages/mall/admin/user/management/index.uvue'
|
||||
@@ -180,7 +184,11 @@ import MaintainSysInfo from '@/pages/mall/admin/maintain/sys/info.uvue'
|
||||
export const componentMap: Map<string, any> = new Map([
|
||||
// 首页
|
||||
['HomeIndex', HomeIndex],
|
||||
['UserCenter', UserCenter],
|
||||
['UserCenter', UserCenter],
|
||||
|
||||
// 店铺模块
|
||||
['ShopManage', ShopManage],
|
||||
['ShopCreate', ShopCreate],
|
||||
|
||||
// 用户模块
|
||||
['UserStatistic', UserStatistic],
|
||||
|
||||
@@ -67,12 +67,22 @@ export const topMenus: TopMenu[] = [
|
||||
order: 1,
|
||||
groups: []
|
||||
},
|
||||
{
|
||||
id: 'shop',
|
||||
title: '店铺',
|
||||
icon: 'shop',
|
||||
path: '/pages/mall/admin/shop/manage',
|
||||
order: 2,
|
||||
groups: [
|
||||
{ id: 'shop-manage', title: '', order: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
title: '用户',
|
||||
icon: 'user',
|
||||
path: '/pages/mall/admin/user/management/index',
|
||||
order: 2,
|
||||
order: 3,
|
||||
groups: [
|
||||
{ id: 'user-manage', title: '', order: 1 }
|
||||
]
|
||||
@@ -233,6 +243,27 @@ export const routes: RouteRecord[] = [
|
||||
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[] {
|
||||
const normRole = normalizeRole(role)
|
||||
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') {
|
||||
return ['home', 'order', 'product', 'marketing', 'finance']
|
||||
return ['home', 'shop', 'order', 'product', 'marketing', 'finance']
|
||||
}
|
||||
|
||||
return ['home']
|
||||
@@ -123,7 +123,7 @@ export function hasAdminModuleAccess(moduleId: string | undefined): boolean {
|
||||
}
|
||||
|
||||
if (normRole === 'merchant') {
|
||||
const allowed = ['home', 'order', 'product', 'marketing', 'finance']
|
||||
const allowed = ['home', 'shop', 'order', 'product', 'marketing', 'finance']
|
||||
return allowed.includes(moduleId)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
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