614 lines
17 KiB
Plaintext
614 lines
17 KiB
Plaintext
<!-- 创建店铺页(入驻申请) -->
|
||
<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>
|