完成店铺创建

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

View File

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