完成店铺创建
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user