Files
medical-mall/pages/mall/admin/shop/create.uvue
2026-03-13 16:32:37 +08:00

614 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 创建店铺页(入驻申请) -->
<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>