逻辑完善
This commit is contained in:
parent
a927b930c1
commit
231bf97bc8
|
|
@ -39,3 +39,15 @@ export function importSpecData(productId: number, file: File) {
|
|||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
}
|
||||
|
||||
export function getSpecDataList(productId: number) {
|
||||
return http.get(`/admin/products/${productId}/spec-data`)
|
||||
}
|
||||
|
||||
export function createSpecData(productId: number, data: any) {
|
||||
return http.post(`/admin/products/${productId}/spec-data`, data)
|
||||
}
|
||||
|
||||
export function deleteSpecData(productId: number, specId: number) {
|
||||
return http.delete(`/admin/products/${productId}/spec-data/${specId}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
<el-menu-item index="/products">
|
||||
<span>商品管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/categories">
|
||||
<span>分类管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/orders">
|
||||
<span>订单管理</span>
|
||||
</el-menu-item>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ const router = createRouter({
|
|||
name: 'MoldList',
|
||||
component: () => import('../views/mold/MoldList.vue'),
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'CategoryList',
|
||||
component: () => import('../views/category/CategoryList.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
117
admin/src/views/category/CategoryList.vue
Normal file
117
admin/src/views/category/CategoryList.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3>分类管理</h3>
|
||||
<div style="margin-bottom: 16px">
|
||||
<el-button type="primary" @click="dialogVisible = true">新增分类</el-button>
|
||||
</div>
|
||||
<el-table :data="categories" v-loading="loading" border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="图标" width="100">
|
||||
<template #default="{ row }">
|
||||
<img v-if="row.icon" :src="row.icon" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover" />
|
||||
<span v-else style="color: #ccc">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="分类名称" />
|
||||
<el-table-column prop="sort" label="排序" width="100" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" text size="small" @click="handleDelete(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 新增分类弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" title="新增分类" width="460px" @close="resetForm">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="分类名称">
|
||||
<el-input v-model="newName" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="newSort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类图标">
|
||||
<el-upload
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleIconSuccess"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="newIcon" :src="newIcon" style="width: 60px; height: 60px; border-radius: 50%; object-fit: cover; cursor: pointer" />
|
||||
<el-button v-else>上传图标</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="adding" @click="handleAdd">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import http, { getUploadUrl } from '../../api/request'
|
||||
|
||||
const categories = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const adding = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const newName = ref('')
|
||||
const newSort = ref(0)
|
||||
const newIcon = ref('')
|
||||
|
||||
const uploadUrl = getUploadUrl()
|
||||
const uploadHeaders = computed(() => ({
|
||||
Authorization: `Bearer ${localStorage.getItem('admin_token') || ''}`,
|
||||
}))
|
||||
|
||||
function handleIconSuccess(response: any) {
|
||||
if (response.code === 0) newIcon.value = response.data.url
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
newName.value = ''
|
||||
newSort.value = 0
|
||||
newIcon.value = ''
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await http.get('/admin/categories')
|
||||
categories.value = res.data
|
||||
} catch { /* handled */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
if (!newName.value.trim()) { ElMessage.warning('请输入分类名称'); return }
|
||||
adding.value = true
|
||||
try {
|
||||
await http.post('/admin/categories', { name: newName.value.trim(), sort: newSort.value, icon: newIcon.value || null })
|
||||
ElMessage.success('添加成功')
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
loadCategories()
|
||||
} catch { ElMessage.error('添加失败') }
|
||||
finally { adding.value = false }
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该分类?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||
await http.delete(`/admin/categories/${id}`)
|
||||
ElMessage.success('删除成功')
|
||||
loadCategories()
|
||||
} catch (err: any) {
|
||||
if (err === 'cancel') return
|
||||
ElMessage.error(err.response?.data?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadCategories)
|
||||
</script>
|
||||
|
|
@ -33,6 +33,24 @@
|
|||
<el-switch v-model="form.status" active-value="on" inactive-value="off" active-text="上架" inactive-text="下架" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 列表展示图 -->
|
||||
<el-form-item label="列表展示图">
|
||||
<el-upload
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
list-type="picture-card"
|
||||
:file-list="thumbFileList"
|
||||
:limit="1"
|
||||
:on-success="handleThumbSuccess"
|
||||
:on-remove="handleThumbRemove"
|
||||
accept="image/*"
|
||||
:class="{ 'hide-upload': thumbFileList.length >= 1 }"
|
||||
>
|
||||
<span>+</span>
|
||||
</el-upload>
|
||||
<div style="color: #999; font-size: 12px">用于商品列表页展示的缩略图</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Banner 多图上传 -->
|
||||
<el-form-item label="Banner 图片">
|
||||
<el-upload
|
||||
|
|
@ -65,22 +83,28 @@
|
|||
<!-- Detail_Parameter 配置 -->
|
||||
<el-divider>详细参数配置</el-divider>
|
||||
<el-form-item label="成色选项">
|
||||
<el-select v-model="detailParams.fineness" multiple filterable allow-create placeholder="输入后回车添加">
|
||||
</el-select>
|
||||
<div class="tag-input-wrap">
|
||||
<el-tag v-for="(tag, i) in detailParams.fineness" :key="tag" closable @close="detailParams.fineness.splice(i, 1)" style="margin-right: 8px; margin-bottom: 4px">{{ tag }}</el-tag>
|
||||
<el-input v-model="tagInputs.fineness" size="small" style="width: 140px" placeholder="输入后回车添加" @keyup.enter="addTag('fineness')" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="主石选项">
|
||||
<el-select v-model="detailParams.mainStone" multiple filterable allow-create placeholder="输入后回车添加">
|
||||
</el-select>
|
||||
<div class="tag-input-wrap">
|
||||
<el-tag v-for="(tag, i) in detailParams.mainStone" :key="tag" closable @close="detailParams.mainStone.splice(i, 1)" style="margin-right: 8px; margin-bottom: 4px">{{ tag }}</el-tag>
|
||||
<el-input v-model="tagInputs.mainStone" size="small" style="width: 140px" placeholder="输入后回车添加" @keyup.enter="addTag('mainStone')" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="手寸选项">
|
||||
<el-select v-model="detailParams.ringSize" multiple filterable allow-create placeholder="输入后回车添加">
|
||||
</el-select>
|
||||
<div class="tag-input-wrap">
|
||||
<el-tag v-for="(tag, i) in detailParams.ringSize" :key="tag" closable @close="detailParams.ringSize.splice(i, 1)" style="margin-right: 8px; margin-bottom: 4px">{{ tag }}</el-tag>
|
||||
<el-input v-model="tagInputs.ringSize" size="small" style="width: 140px" placeholder="输入后回车添加" @keyup.enter="addTag('ringSize')" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Spec Data Import/Export (edit mode only) -->
|
||||
<!-- Spec Data Management (edit mode only) -->
|
||||
<template v-if="isEdit">
|
||||
<el-divider>规格数据管理</el-divider>
|
||||
<el-form-item label="规格数据">
|
||||
<el-form-item label="导入导出">
|
||||
<el-button @click="handleExport">导出 CSV</el-button>
|
||||
<el-upload
|
||||
:action="`${apiBaseUrl}/admin/products/${route.params.id}/spec-data/import`"
|
||||
|
|
@ -93,9 +117,100 @@
|
|||
>
|
||||
<el-button type="success">导入 CSV</el-button>
|
||||
</el-upload>
|
||||
<el-button type="primary" style="margin-left: 8px" @click="specDialogVisible = true">新增规格</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 规格数据表格 -->
|
||||
<el-form-item label="规格列表" v-if="specDataRows.length">
|
||||
<el-table :data="specDataRows" border size="small" style="width: 100%" max-height="500">
|
||||
<el-table-column prop="modelName" label="型号名称" width="120" fixed />
|
||||
<el-table-column prop="fineness" label="成色" width="80" />
|
||||
<el-table-column prop="mainStone" label="主石" width="80" />
|
||||
<el-table-column prop="ringSize" label="手寸" width="70" />
|
||||
<el-table-column prop="goldTotalWeight" label="金料总重" width="90" />
|
||||
<el-table-column prop="goldNetWeight" label="金料净重" width="90" />
|
||||
<el-table-column prop="loss" label="损耗" width="70" />
|
||||
<el-table-column prop="goldLoss" label="金耗" width="70" />
|
||||
<el-table-column prop="goldPrice" label="金价" width="80" />
|
||||
<el-table-column prop="goldValue" label="金值" width="80" />
|
||||
<el-table-column prop="mainStoneCount" label="主石数量" width="80" />
|
||||
<el-table-column prop="mainStoneWeight" label="主石石重" width="80" />
|
||||
<el-table-column prop="mainStoneUnitPrice" label="主石单价" width="80" />
|
||||
<el-table-column prop="mainStoneAmount" label="主石金额" width="80" />
|
||||
<el-table-column prop="sideStoneCount" label="副石数量" width="80" />
|
||||
<el-table-column prop="sideStoneWeight" label="副石石重" width="80" />
|
||||
<el-table-column prop="sideStoneUnitPrice" label="副石单价" width="80" />
|
||||
<el-table-column prop="sideStoneAmount" label="副石金额" width="80" />
|
||||
<el-table-column prop="accessoryAmount" label="配件金额" width="80" />
|
||||
<el-table-column prop="processingFee" label="加工工费" width="80" />
|
||||
<el-table-column prop="settingFee" label="镶石工费" width="80" />
|
||||
<el-table-column prop="totalLaborCost" label="总工费" width="80" />
|
||||
<el-table-column prop="totalPrice" label="总价" width="80" />
|
||||
<el-table-column label="操作" width="80" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" text size="small" @click="handleDeleteSpec(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="规格列表">
|
||||
<span style="color: #999">暂无规格数据,请通过 CSV 导入或手动新增</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 新增规格弹窗 -->
|
||||
<el-dialog v-model="specDialogVisible" title="新增规格数据" width="700px" @close="resetSpecForm">
|
||||
<el-form :model="specForm" label-width="100px" size="small">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="型号名称"><el-input v-model="specForm.modelName" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="成色"><el-input v-model="specForm.fineness" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="主石"><el-input v-model="specForm.mainStone" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="手寸"><el-input v-model="specForm.ringSize" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider>金料信息</el-divider>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="金料总重"><el-input-number v-model="specForm.goldTotalWeight" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金料净重"><el-input-number v-model="specForm.goldNetWeight" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="损耗"><el-input-number v-model="specForm.loss" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="金耗"><el-input-number v-model="specForm.goldLoss" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金价"><el-input-number v-model="specForm.goldPrice" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金值"><el-input-number v-model="specForm.goldValue" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider>主石信息</el-divider>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="数量"><el-input-number v-model="specForm.mainStoneCount" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="石重"><el-input-number v-model="specForm.mainStoneWeight" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="单价"><el-input-number v-model="specForm.mainStoneUnitPrice" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="金额"><el-input-number v-model="specForm.mainStoneAmount" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider>副石信息</el-divider>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="数量"><el-input-number v-model="specForm.sideStoneCount" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="石重"><el-input-number v-model="specForm.sideStoneWeight" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="单价"><el-input-number v-model="specForm.sideStoneUnitPrice" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="金额"><el-input-number v-model="specForm.sideStoneAmount" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider>工费信息</el-divider>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="配件金额"><el-input-number v-model="specForm.accessoryAmount" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="加工工费"><el-input-number v-model="specForm.processingFee" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="镶石工费"><el-input-number v-model="specForm.settingFee" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="总工费"><el-input-number v-model="specForm.totalLaborCost" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="总价"><el-input-number v-model="specForm.totalPrice" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="specDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="specSaving" @click="handleCreateSpec">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmit">保存</el-button>
|
||||
<el-button @click="$router.back()">取消</el-button>
|
||||
|
|
@ -107,8 +222,8 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type UploadFile } from 'element-plus'
|
||||
import { getProductDetail, createProduct, updateProduct, getCategories, exportSpecData } from '../../api/product'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type UploadFile } from 'element-plus'
|
||||
import { getProductDetail, createProduct, updateProduct, getCategories, exportSpecData, getSpecDataList, createSpecData, deleteSpecData } from '../../api/product'
|
||||
import http, { getUploadUrl } from '../../api/request'
|
||||
|
||||
const route = useRoute()
|
||||
|
|
@ -136,6 +251,7 @@ const form = reactive({
|
|||
status: 'on' as 'on' | 'off',
|
||||
bannerImages: [] as string[],
|
||||
bannerVideo: '',
|
||||
thumb: '',
|
||||
})
|
||||
|
||||
const detailParams = reactive({
|
||||
|
|
@ -144,12 +260,79 @@ const detailParams = reactive({
|
|||
ringSize: [] as string[],
|
||||
})
|
||||
|
||||
const tagInputs = reactive({ fineness: '', mainStone: '', ringSize: '' })
|
||||
|
||||
function addTag(key: 'fineness' | 'mainStone' | 'ringSize') {
|
||||
const val = tagInputs[key].trim()
|
||||
if (val && !detailParams[key].includes(val)) {
|
||||
detailParams[key].push(val)
|
||||
}
|
||||
tagInputs[key] = ''
|
||||
}
|
||||
|
||||
const bannerFileList = ref<UploadFile[]>([])
|
||||
const thumbFileList = ref<UploadFile[]>([])
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
// Spec data management
|
||||
const specDataRows = ref<any[]>([])
|
||||
const specDialogVisible = ref(false)
|
||||
const specSaving = ref(false)
|
||||
const specForm = reactive({
|
||||
modelName: '', fineness: '', mainStone: '', ringSize: '',
|
||||
goldTotalWeight: 0, goldNetWeight: 0, loss: 0, goldLoss: 0,
|
||||
goldPrice: 0, goldValue: 0,
|
||||
mainStoneCount: 0, mainStoneWeight: 0, mainStoneUnitPrice: 0, mainStoneAmount: 0,
|
||||
sideStoneCount: 0, sideStoneWeight: 0, sideStoneUnitPrice: 0, sideStoneAmount: 0,
|
||||
accessoryAmount: 0, processingFee: 0, settingFee: 0, totalLaborCost: 0, totalPrice: 0,
|
||||
})
|
||||
|
||||
function resetSpecForm() {
|
||||
Object.assign(specForm, {
|
||||
modelName: '', fineness: '', mainStone: '', ringSize: '',
|
||||
goldTotalWeight: 0, goldNetWeight: 0, loss: 0, goldLoss: 0,
|
||||
goldPrice: 0, goldValue: 0,
|
||||
mainStoneCount: 0, mainStoneWeight: 0, mainStoneUnitPrice: 0, mainStoneAmount: 0,
|
||||
sideStoneCount: 0, sideStoneWeight: 0, sideStoneUnitPrice: 0, sideStoneAmount: 0,
|
||||
accessoryAmount: 0, processingFee: 0, settingFee: 0, totalLaborCost: 0, totalPrice: 0,
|
||||
})
|
||||
}
|
||||
|
||||
async function loadSpecData() {
|
||||
if (!isEdit.value) return
|
||||
try {
|
||||
const res: any = await getSpecDataList(Number(route.params.id))
|
||||
specDataRows.value = res.data
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function handleCreateSpec() {
|
||||
specSaving.value = true
|
||||
try {
|
||||
await createSpecData(Number(route.params.id), { ...specForm })
|
||||
ElMessage.success('新增成功')
|
||||
specDialogVisible.value = false
|
||||
resetSpecForm()
|
||||
loadSpecData()
|
||||
} catch { ElMessage.error('新增失败') }
|
||||
finally { specSaving.value = false }
|
||||
}
|
||||
|
||||
async function handleDeleteSpec(specId: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该规格数据?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||
await deleteSpecData(Number(route.params.id), specId)
|
||||
ElMessage.success('删除成功')
|
||||
loadSpecData()
|
||||
} catch (err: any) {
|
||||
if (err === 'cancel') return
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleBannerSuccess(response: any) {
|
||||
if (response.code === 0) {
|
||||
form.bannerImages.push(response.data.url)
|
||||
|
|
@ -162,6 +345,16 @@ function handleBannerRemove(_file: UploadFile, fileList: UploadFile[]) {
|
|||
.map((f: any) => f.response?.data?.url || f.url)
|
||||
}
|
||||
|
||||
function handleThumbSuccess(response: any) {
|
||||
if (response.code === 0) {
|
||||
form.thumb = response.data.url
|
||||
}
|
||||
}
|
||||
|
||||
function handleThumbRemove() {
|
||||
form.thumb = ''
|
||||
}
|
||||
|
||||
function handleVideoSuccess(response: any) {
|
||||
if (response.code === 0) {
|
||||
form.bannerVideo = response.data.url
|
||||
|
|
@ -185,18 +378,20 @@ async function loadProduct() {
|
|||
status: p.status,
|
||||
bannerImages: p.banner_images || [],
|
||||
bannerVideo: p.banner_video || '',
|
||||
thumb: p.thumb || '',
|
||||
})
|
||||
bannerFileList.value = (p.banner_images || []).map((url: string, i: number) => ({
|
||||
name: `image-${i}`,
|
||||
url,
|
||||
}))
|
||||
thumbFileList.value = p.thumb ? [{ name: 'thumb', url: p.thumb }] as UploadFile[] : []
|
||||
|
||||
// Load detail params
|
||||
const specRes: any = await http.get(`/products/${route.params.id}/specs`)
|
||||
if (specRes.data) {
|
||||
detailParams.fineness = specRes.data.fineness || []
|
||||
detailParams.mainStone = specRes.data.main_stone || []
|
||||
detailParams.ringSize = specRes.data.ring_size || []
|
||||
detailParams.mainStone = specRes.data.mainStone || specRes.data.main_stone || []
|
||||
detailParams.ringSize = specRes.data.ringSize || specRes.data.ring_size || []
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('加载商品信息失败')
|
||||
|
|
@ -250,6 +445,7 @@ async function handleExport() {
|
|||
function handleImportSuccess(response: any) {
|
||||
if (response.code === 0) {
|
||||
ElMessage.success(`导入成功,共 ${response.data.imported} 条`)
|
||||
loadSpecData()
|
||||
} else {
|
||||
ElMessage.error(response.message || '导入失败')
|
||||
}
|
||||
|
|
@ -265,5 +461,18 @@ onMounted(async () => {
|
|||
categories.value = res.data
|
||||
} catch { /* ignore */ }
|
||||
loadProduct()
|
||||
loadSpecData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hide-upload :deep(.el-upload--picture-card) {
|
||||
display: none;
|
||||
}
|
||||
.tag-input-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const getProductSpecs = (id: number) =>
|
|||
get<DetailParameterConfig>(`/api/products/${id}/specs`)
|
||||
|
||||
/** 根据参数组合获取规格数据列表 */
|
||||
export const getSpecDataList = (id: number, params: { fineness: string; mainStone: string; ringSize: string }) =>
|
||||
export const getSpecDataList = (id: number, params: { fineness?: string; mainStone?: string; ringSize?: string }) =>
|
||||
post<SpecData[]>(`/api/products/${id}/spec-data`, params as unknown as Record<string, unknown>)
|
||||
|
||||
/** 获取商品分类列表 */
|
||||
|
|
|
|||
|
|
@ -1,34 +1,31 @@
|
|||
<template>
|
||||
<swiper class="banner-swiper" :indicator-dots="true" :autoplay="true" :interval="3000" circular>
|
||||
<!-- 视频项 -->
|
||||
<swiper-item v-if="video">
|
||||
<video class="banner-swiper__video" :src="video" controls />
|
||||
<video class="banner-swiper__video" :src="fullUrl(video)" controls />
|
||||
</swiper-item>
|
||||
<!-- 图片项 -->
|
||||
<swiper-item v-for="(img, idx) in images" :key="idx">
|
||||
<image class="banner-swiper__image" :src="img" mode="aspectFill" />
|
||||
<image class="banner-swiper__image" :src="fullUrl(img)" mode="aspectFill" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BASE_URL } from '../utils/request'
|
||||
|
||||
defineProps<{
|
||||
images: string[]
|
||||
video?: string
|
||||
}>()
|
||||
|
||||
function fullUrl(path: string): string {
|
||||
if (!path) return ''
|
||||
if (path.startsWith('http')) return path
|
||||
return BASE_URL + path
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.banner-swiper {
|
||||
width: 100%;
|
||||
height: 600rpx;
|
||||
}
|
||||
.banner-swiper__image {
|
||||
width: 100%;
|
||||
height: 600rpx;
|
||||
}
|
||||
.banner-swiper__video {
|
||||
width: 100%;
|
||||
height: 600rpx;
|
||||
}
|
||||
.banner-swiper { width: 100%; height: 600rpx; }
|
||||
.banner-swiper__image { width: 100%; height: 600rpx; }
|
||||
.banner-swiper__video { width: 100%; height: 600rpx; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,76 +1,85 @@
|
|||
<template>
|
||||
<view class="product-card" @click="goDetail">
|
||||
<image
|
||||
class="product-card__image"
|
||||
:src="product.bannerImages[0] || '/static/logo.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-card__info">
|
||||
<text class="product-card__name">{{ product.name }}</text>
|
||||
<text class="product-card__style">款号:{{ product.styleNo }}</text>
|
||||
<view class="product-card__bottom">
|
||||
<text class="product-card__price">¥{{ product.basePrice }}</text>
|
||||
<text class="product-card__stock">库存 {{ product.stock }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="product-card" @click="goDetail">
|
||||
<image class="product-card__image" :src="imgSrc()" mode="aspectFill" />
|
||||
<view class="product-card__info">
|
||||
<text class="product-card__name">{{ product.name }}({{ product.styleNo }})</text>
|
||||
<view class="product-card__bottom">
|
||||
<view class="product-card__price-tag">
|
||||
<text class="product-card__price">¥{{ product.basePrice }}</text>
|
||||
</view>
|
||||
<text class="product-card__stock">库存{{ product.stock }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Product } from '../types'
|
||||
import type { Product } from '../types'
|
||||
import { BASE_URL } from '../utils/request'
|
||||
|
||||
const props = defineProps<{
|
||||
product: Product
|
||||
}>()
|
||||
const props = defineProps<{ product : Product }>()
|
||||
|
||||
function goDetail() {
|
||||
uni.navigateTo({ url: `/pages/product/detail?id=${props.product.id}` })
|
||||
}
|
||||
function imgSrc() : string {
|
||||
const url = props.product.thumb || props.product.bannerImages?.[0]
|
||||
if (!url) return '/static/logo.png'
|
||||
if (url.startsWith('http')) return url
|
||||
return BASE_URL + url
|
||||
}
|
||||
function goDetail() {
|
||||
uni.navigateTo({ url: `/pages/product/detail?id=${props.product.id}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.product-card__image {
|
||||
width: 100%;
|
||||
height: 340rpx;
|
||||
}
|
||||
.product-card__info {
|
||||
padding: 16rpx;
|
||||
}
|
||||
.product-card__name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
.product-card__style {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
.product-card__bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.product-card__price {
|
||||
font-size: 32rpx;
|
||||
color: #e4393c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.product-card__stock {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
.product-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-card__image {
|
||||
width: 100%;
|
||||
height: 340rpx;
|
||||
}
|
||||
|
||||
.product-card__info {
|
||||
padding: 16rpx 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.product-card__name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.product-card__bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.product-card__price-tag {
|
||||
background: linear-gradient(135deg, #f5a0b8, #FF6D9B);
|
||||
border-radius: 8rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
}
|
||||
|
||||
.product-card__price {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-card__stock {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
<template>
|
||||
<view class="shipping-notice">
|
||||
<view class="shipping-notice__title">发货公告</view>
|
||||
<view class="shipping-notice__item">交易方式:线下支付,微信沟通确认</view>
|
||||
<view class="shipping-notice__item">客服微信:请点击「联系客服」获取</view>
|
||||
<view class="shipping-notice__item">公司地址:请联系客服获取详细地址</view>
|
||||
<view class="shipping-notice__header">
|
||||
<image class="shipping-notice__icon" src="/static/ic_notice.png" mode="aspectFit" />
|
||||
<text class="shipping-notice__title">发货公告:</text>
|
||||
</view>
|
||||
<view class="shipping-notice__body">
|
||||
<text class="shipping-notice__item">叶生珠宝-空托之城空托都是当天金工石结算</text>
|
||||
<text class="shipping-notice__item">客服微信:15920028399</text>
|
||||
<text class="shipping-notice__item">交易方式:加微信门店交易,支付宝,微信,银行卡转账</text>
|
||||
<text class="shipping-notice__item">公司地址:深圳市罗湖区水贝二路贝丽花园21栋108叶生珠宝</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
|
@ -12,20 +18,34 @@
|
|||
|
||||
<style scoped>
|
||||
.shipping-notice {
|
||||
background: #fffbe6;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
margin: 20rpx 24rpx;
|
||||
background: #fce4ec;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
margin: 20rpx 24rpx 0;
|
||||
}
|
||||
.shipping-notice__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.shipping-notice__icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.shipping-notice__title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #fa8c16;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 600;
|
||||
color: #e91e63;
|
||||
}
|
||||
.shipping-notice__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.shipping-notice__item {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 40rpx;
|
||||
font-size: 26rpx;
|
||||
color: #e91e63;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,281 +1,317 @@
|
|||
<template>
|
||||
<view class="spec-panel-mask" @click.self="$emit('close')">
|
||||
<view class="spec-panel">
|
||||
<view class="spec-panel__header">
|
||||
<text class="spec-panel__title">详细参数</text>
|
||||
<text class="spec-panel__close" @click="$emit('close')">✕</text>
|
||||
</view>
|
||||
<view class="spec-panel" @click.stop>
|
||||
<!-- 滚动区域 -->
|
||||
<scroll-view class="spec-panel__scroll" scroll-y>
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loadingConfig" class="spec-panel__loading">加载中...</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loadingConfig" class="spec-panel__loading">加载中...</view>
|
||||
|
||||
<template v-else-if="config">
|
||||
<!-- 成色选择 -->
|
||||
<view class="spec-group" v-if="config.fineness.length">
|
||||
<text class="spec-group__label">成色</text>
|
||||
<view class="spec-group__options">
|
||||
<view
|
||||
v-for="item in config.fineness"
|
||||
:key="item"
|
||||
class="spec-option"
|
||||
:class="{ 'spec-option--active': selected.fineness === item }"
|
||||
@click="selected.fineness = item"
|
||||
>
|
||||
{{ item }}
|
||||
<template v-else-if="config">
|
||||
<!-- 成色选择 -->
|
||||
<view class="spec-group" v-if="config.fineness.length">
|
||||
<text class="spec-group__label">成 色</text>
|
||||
<view class="spec-group__options">
|
||||
<view v-for="item in config.fineness" :key="item" class="spec-option"
|
||||
:class="{ 'spec-option--active': selected.fineness === item }"
|
||||
@click="selectOption('fineness', item)">{{ item }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主石选择 -->
|
||||
<view class="spec-group" v-if="config.mainStone.length">
|
||||
<text class="spec-group__label">主石</text>
|
||||
<view class="spec-group__options">
|
||||
<view
|
||||
v-for="item in config.mainStone"
|
||||
:key="item"
|
||||
class="spec-option"
|
||||
:class="{ 'spec-option--active': selected.mainStone === item }"
|
||||
@click="selected.mainStone = item"
|
||||
>
|
||||
{{ item }}
|
||||
<!-- 主石选择 -->
|
||||
<view class="spec-group" v-if="config.mainStone.length">
|
||||
<text class="spec-group__label">主 石</text>
|
||||
<view class="spec-group__options">
|
||||
<view v-for="item in config.mainStone" :key="item" class="spec-option"
|
||||
:class="{ 'spec-option--active': selected.mainStone === item }"
|
||||
@click="selectOption('mainStone', item)">{{ item }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手寸选择 -->
|
||||
<view class="spec-group" v-if="config.ringSize.length">
|
||||
<text class="spec-group__label">手寸</text>
|
||||
<view class="spec-group__options">
|
||||
<view
|
||||
v-for="item in config.ringSize"
|
||||
:key="item"
|
||||
class="spec-option"
|
||||
:class="{ 'spec-option--active': selected.ringSize === item }"
|
||||
@click="selected.ringSize = item"
|
||||
>
|
||||
{{ item }}
|
||||
<!-- 手寸选择 -->
|
||||
<view class="spec-group" v-if="config.ringSize.length">
|
||||
<text class="spec-group__label">手 寸</text>
|
||||
<view class="spec-group__options">
|
||||
<view v-for="item in config.ringSize" :key="item" class="spec-option"
|
||||
:class="{ 'spec-option--active': selected.ringSize === item }"
|
||||
@click="selectOption('ringSize', item)">{{ item }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 查询按钮 -->
|
||||
<view class="spec-panel__action">
|
||||
<view
|
||||
class="spec-panel__btn"
|
||||
:class="{ 'spec-panel__btn--disabled': !canQuery }"
|
||||
@click="querySpecData"
|
||||
>
|
||||
查询规格数据
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 规格数据列表 -->
|
||||
<view v-if="loadingData" class="spec-panel__loading">查询中...</view>
|
||||
<view v-else-if="specDataList.length" class="spec-data-list">
|
||||
<view v-for="spec in specDataList" :key="spec.id" class="spec-data-item">
|
||||
<view class="spec-data-row">
|
||||
<text class="spec-data-label">型号</text>
|
||||
<text class="spec-data-value">{{ spec.modelName }}</text>
|
||||
<view v-for="spec in specDataList" :key="spec.id" class="spec-data-card"
|
||||
:class="{ 'spec-data-card--selected': selectedSpecs.has(spec.id) }"
|
||||
@click="toggleSelect(spec)">
|
||||
<!-- 卡片标题 -->
|
||||
<view class="spec-card__header">
|
||||
<text class="spec-card__title">{{ spec.modelName }} {{ spec.fineness }}/外径</text>
|
||||
<text class="spec-card__gold-price">金价 ¥{{ spec.goldPrice }}</text>
|
||||
</view>
|
||||
<view class="spec-data-row">
|
||||
<text class="spec-data-label">金料总重</text>
|
||||
<text class="spec-data-value">{{ spec.goldTotalWeight }}g</text>
|
||||
<!-- 两列数据 -->
|
||||
<view class="spec-card__body">
|
||||
<view class="spec-card__col">
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">型号名称</text>
|
||||
<text class="spec-card__value">{{ spec.modelName }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">金料总重</text>
|
||||
<text class="spec-card__value">{{ spec.goldTotalWeight }}g</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">金料净重</text>
|
||||
<text class="spec-card__value">{{ spec.goldNetWeight }}g</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">金耗</text>
|
||||
<text class="spec-card__value">{{ spec.goldLoss }}g</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">金值</text>
|
||||
<text class="spec-card__value">¥{{ spec.goldValue }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">主石数量</text>
|
||||
<text class="spec-card__value">{{ spec.mainStoneCount }}粒</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">主石石重</text>
|
||||
<text class="spec-card__value">{{ spec.mainStoneWeight }}ct</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">主石单价</text>
|
||||
<text class="spec-card__value">¥{{ spec.mainStoneUnitPrice }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">主石金额</text>
|
||||
<text class="spec-card__value">¥{{ spec.mainStoneAmount }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">配件金额</text>
|
||||
<text class="spec-card__value">¥{{ spec.accessoryAmount }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">总工费</text>
|
||||
<text class="spec-card__value">¥{{ spec.totalLaborCost }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="spec-card__col">
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">成色</text>
|
||||
<text class="spec-card__value">{{ spec.fineness }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">损耗</text>
|
||||
<text class="spec-card__value">{{ spec.loss }}%</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">主石</text>
|
||||
<text class="spec-card__value">{{ spec.mainStone }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">手寸</text>
|
||||
<text class="spec-card__value">{{ spec.ringSize }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">金价</text>
|
||||
<text class="spec-card__value">¥{{ spec.goldPrice }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">副石数量</text>
|
||||
<text class="spec-card__value">{{ spec.sideStoneCount }}粒</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">副石石重</text>
|
||||
<text class="spec-card__value">{{ spec.sideStoneWeight }}ct</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">副石单价</text>
|
||||
<text class="spec-card__value">¥{{ spec.sideStoneUnitPrice }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">副石金额</text>
|
||||
<text class="spec-card__value">¥{{ spec.sideStoneAmount }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">加工工费</text>
|
||||
<text class="spec-card__value">¥{{ spec.processingFee }}</text>
|
||||
</view>
|
||||
<view class="spec-card__row">
|
||||
<text class="spec-card__label">镶石工费</text>
|
||||
<text class="spec-card__value">¥{{ spec.settingFee }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="spec-data-row">
|
||||
<text class="spec-data-label">金值</text>
|
||||
<text class="spec-data-value">¥{{ spec.goldValue }}</text>
|
||||
</view>
|
||||
<view class="spec-data-row">
|
||||
<text class="spec-data-label">主石金额</text>
|
||||
<text class="spec-data-value">¥{{ spec.mainStoneAmount }}</text>
|
||||
</view>
|
||||
<view class="spec-data-row">
|
||||
<text class="spec-data-label">副石金额</text>
|
||||
<text class="spec-data-value">¥{{ spec.sideStoneAmount }}</text>
|
||||
</view>
|
||||
<view class="spec-data-row">
|
||||
<text class="spec-data-label">总工费</text>
|
||||
<text class="spec-data-value">¥{{ spec.totalLaborCost }}</text>
|
||||
</view>
|
||||
<view class="spec-data-row spec-data-row--total">
|
||||
<text class="spec-data-label">总价</text>
|
||||
<text class="spec-data-value spec-data-value--price">¥{{ spec.totalPrice }}</text>
|
||||
<!-- 总价行 -->
|
||||
<view class="spec-card__footer">
|
||||
<text class="spec-card__total-label">金工石总金额</text>
|
||||
<text class="spec-card__total-price">¥{{ spec.totalPrice }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="spec-panel__bottom">
|
||||
<view class="spec-panel__cart-btn"
|
||||
:class="{ 'spec-panel__cart-btn--disabled': selectedSpecs.size === 0 }"
|
||||
@click="handleAddToCart">
|
||||
加入购物车<text v-if="selectedSpecs.size">({{ selectedSpecs.size }})</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import type { DetailParameterConfig, SpecData } from '../types'
|
||||
import { getProductSpecs, getSpecDataList } from '../api/product'
|
||||
import { useCartStore } from '../store/cart'
|
||||
|
||||
const props = defineProps<{
|
||||
productId: number
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
const props = defineProps<{ productId: number }>()
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
|
||||
const config = ref<DetailParameterConfig | null>(null)
|
||||
const loadingConfig = ref(false)
|
||||
const loadingData = ref(false)
|
||||
const specDataList = ref<SpecData[]>([])
|
||||
const selectedSpecs = ref<Map<number, SpecData>>(new Map())
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const selected = reactive({
|
||||
fineness: '',
|
||||
mainStone: '',
|
||||
ringSize: '',
|
||||
})
|
||||
const selected = reactive({ fineness: '', mainStone: '', ringSize: '' })
|
||||
|
||||
const canQuery = computed(() =>
|
||||
selected.fineness && selected.mainStone && selected.ringSize
|
||||
)
|
||||
function selectOption(key: 'fineness' | 'mainStone' | 'ringSize', item: string) {
|
||||
selected[key] = selected[key] === item ? '' : item
|
||||
if (selected.fineness || selected.mainStone || selected.ringSize) {
|
||||
querySpecData()
|
||||
} else {
|
||||
specDataList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelect(spec: SpecData) {
|
||||
if (selectedSpecs.value.has(spec.id)) {
|
||||
selectedSpecs.value.delete(spec.id)
|
||||
} else {
|
||||
selectedSpecs.value.set(spec.id, spec)
|
||||
}
|
||||
selectedSpecs.value = new Map(selectedSpecs.value)
|
||||
}
|
||||
|
||||
function handleAddToCart() {
|
||||
if (selectedSpecs.value.size === 0) return
|
||||
selectedSpecs.value.forEach((spec) => {
|
||||
cartStore.addToCart({
|
||||
id: Date.now() + spec.id,
|
||||
userId: 0,
|
||||
productId: props.productId,
|
||||
specDataId: spec.id,
|
||||
quantity: 1,
|
||||
product: {} as any,
|
||||
specData: spec,
|
||||
checked: true,
|
||||
})
|
||||
})
|
||||
uni.showToast({ title: `已加入${selectedSpecs.value.size}件`, icon: 'success' })
|
||||
selectedSpecs.value = new Map()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadingConfig.value = true
|
||||
try {
|
||||
config.value = await getProductSpecs(props.productId)
|
||||
} catch {
|
||||
// 错误已在 request 中统一处理
|
||||
} finally {
|
||||
loadingConfig.value = false
|
||||
}
|
||||
try { config.value = await getProductSpecs(props.productId) }
|
||||
catch { /* handled */ }
|
||||
finally { loadingConfig.value = false }
|
||||
})
|
||||
|
||||
async function querySpecData() {
|
||||
if (!canQuery.value) return
|
||||
loadingData.value = true
|
||||
try {
|
||||
specDataList.value = await getSpecDataList(props.productId, {
|
||||
fineness: selected.fineness,
|
||||
mainStone: selected.mainStone,
|
||||
ringSize: selected.ringSize,
|
||||
fineness: selected.fineness || undefined,
|
||||
mainStone: selected.mainStone || undefined,
|
||||
ringSize: selected.ringSize || undefined,
|
||||
})
|
||||
} catch {
|
||||
// 错误已在 request 中统一处理
|
||||
} finally {
|
||||
loadingData.value = false
|
||||
}
|
||||
} catch { /* handled */ }
|
||||
finally { loadingData.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.spec-panel-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5); z-index: 999;
|
||||
display: flex; align-items: flex-end;
|
||||
}
|
||||
.spec-panel {
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
background: #fff; width: 100%; height: 85vh;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding: 32rpx 24rpx;
|
||||
overflow-y: auto;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.spec-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.spec-panel__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.spec-panel__close {
|
||||
font-size: 36rpx;
|
||||
color: #999;
|
||||
padding: 8rpx;
|
||||
.spec-panel__scroll {
|
||||
flex: 1; padding: 32rpx 28rpx 0; overflow: hidden;
|
||||
}
|
||||
.spec-panel__loading {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.spec-group {
|
||||
margin-bottom: 24rpx;
|
||||
text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 规格分组 */
|
||||
.spec-group { margin-bottom: 32rpx; }
|
||||
.spec-group__label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
.spec-group__options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
font-size: 30rpx; color: #333; font-weight: 600;
|
||||
margin-bottom: 20rpx; display: block; letter-spacing: 8rpx;
|
||||
}
|
||||
.spec-group__options { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
.spec-option {
|
||||
padding: 12rpx 28rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid transparent;
|
||||
padding: 14rpx 32rpx; font-size: 26rpx; color: #333;
|
||||
background: #f5f5f5; border-radius: 8rpx; border: 2rpx solid #f5f5f5;
|
||||
}
|
||||
.spec-option--active {
|
||||
color: #e4393c;
|
||||
background: #fff1f0;
|
||||
border-color: #e4393c;
|
||||
color: #e91e63; background: #fce4ec; border-color: #e91e63;
|
||||
}
|
||||
.spec-panel__action {
|
||||
margin: 24rpx 0;
|
||||
|
||||
/* 规格数据卡片 */
|
||||
.spec-data-list { padding-bottom: 20rpx; }
|
||||
.spec-data-card {
|
||||
border: 2rpx solid #f0e0e0; border-radius: 16rpx;
|
||||
padding: 24rpx; margin-bottom: 20rpx; background: #fff;
|
||||
}
|
||||
.spec-panel__btn {
|
||||
background: #e4393c;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 44rpx;
|
||||
font-size: 28rpx;
|
||||
.spec-data-card--selected {
|
||||
border-color: #e91e63; background: #fff5f7;
|
||||
}
|
||||
.spec-panel__btn--disabled {
|
||||
background: #ccc;
|
||||
.spec-card__header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 16rpx; padding-bottom: 16rpx; border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.spec-data-list {
|
||||
margin-top: 16rpx;
|
||||
.spec-card__title { font-size: 26rpx; color: #333; font-weight: 600; }
|
||||
.spec-card__gold-price { font-size: 24rpx; color: #666; }
|
||||
.spec-card__body { display: flex; gap: 12rpx; }
|
||||
.spec-card__col { flex: 1; }
|
||||
.spec-card__row { display: flex; justify-content: space-between; padding: 5rpx 0; }
|
||||
.spec-card__label { font-size: 23rpx; color: #999; flex-shrink: 0; }
|
||||
.spec-card__value { font-size: 23rpx; color: #333; text-align: right; }
|
||||
.spec-card__footer {
|
||||
display: flex; justify-content: flex-end; align-items: center;
|
||||
margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid #f0f0f0; gap: 12rpx;
|
||||
}
|
||||
.spec-data-item {
|
||||
background: #fafafa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
.spec-card__total-label { font-size: 26rpx; color: #e91e63; }
|
||||
.spec-card__total-price { font-size: 32rpx; color: #e91e63; font-weight: bold; }
|
||||
|
||||
/* 底部操作栏 */
|
||||
.spec-panel__bottom {
|
||||
padding: 20rpx 28rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #fff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
.spec-data-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6rpx 0;
|
||||
.spec-panel__cart-btn {
|
||||
background: linear-gradient(to right, #FFB6C8, #FF6D9B);
|
||||
color: #fff; text-align: center; padding: 24rpx 0;
|
||||
border-radius: 44rpx; font-size: 30rpx; font-weight: 500;
|
||||
}
|
||||
.spec-data-row--total {
|
||||
border-top: 1rpx solid #eee;
|
||||
margin-top: 8rpx;
|
||||
padding-top: 12rpx;
|
||||
}
|
||||
.spec-data-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
.spec-data-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
.spec-data-value--price {
|
||||
color: #e4393c;
|
||||
font-weight: bold;
|
||||
.spec-panel__cart-btn--disabled {
|
||||
background: linear-gradient(to right, #ddd, #ccc);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "珠宝商城"
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/product/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "商品详情"
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,131 +1,309 @@
|
|||
<template>
|
||||
<view class="home-page">
|
||||
<!-- 分类筛选 -->
|
||||
<scroll-view class="category-bar" scroll-x>
|
||||
<view
|
||||
v-for="cat in categories"
|
||||
:key="cat.id"
|
||||
class="category-item"
|
||||
:class="{ 'category-item--active': activeCategoryId === cat.id }"
|
||||
@click="selectCategory(cat.id)"
|
||||
>
|
||||
{{ cat.name }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="home-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="custom-navbar__content" :style="{ height: navBarHeight + 'px' }">
|
||||
<text class="custom-navbar__title">凯缘钻之城</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="product-grid">
|
||||
<view v-for="product in products" :key="product.id" class="product-grid__item">
|
||||
<ProductCard :product="product" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-bar__input" @click="goSearch">
|
||||
<image class="search-bar__icon" src="/static/ic_search.png" mode="aspectFit" />
|
||||
<text class="search-bar__placeholder">请输入产品名称、款号、条码号或款式</text>
|
||||
</view>
|
||||
<text class="search-bar__btn" @click="goSearch">搜索</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && products.length === 0" class="empty-tip">
|
||||
<text>暂无商品</text>
|
||||
</view>
|
||||
<!-- 分类图标区 -->
|
||||
<scroll-view class="category-section" scroll-x>
|
||||
<view class="category-section__inner">
|
||||
<view v-for="cat in categories" :key="cat.id" class="category-icon"
|
||||
:class="{ 'category-icon--active': activeCategoryId === cat.id }" @click="selectCategory(cat.id)">
|
||||
<view class="category-icon__circle">
|
||||
<image v-if="cat.icon" class="category-icon__img" :src="fullUrl(cat.icon)" mode="aspectFill" />
|
||||
<text v-else class="category-icon__emoji">💎</text>
|
||||
</view>
|
||||
<text class="category-icon__label">{{ cat.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loading" class="loading-tip">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 快捷入口 -->
|
||||
<view class="quick-actions">
|
||||
<view class="quick-action quick-action--calc" @click="goCalculator">
|
||||
<text class="quick-action__icon">💎</text>
|
||||
<text class="quick-action__text">钻戒计算器</text>
|
||||
</view>
|
||||
<view class="quick-action quick-action--service" @click="showQrcode = true">
|
||||
<text class="quick-action__icon">👩💼</text>
|
||||
<text class="quick-action__text">客服找款</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="product-grid">
|
||||
<view v-for="product in products" :key="product.id" class="product-grid__item">
|
||||
<ProductCard :product="product" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && products.length === 0" class="empty-tip">
|
||||
<text>暂无商品</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loading" class="loading-tip">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 客服二维码弹窗 -->
|
||||
<CustomerServiceBtn v-if="showQrcode" mode="qrcode" @close="showQrcode = false" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { Product, Category } from '../../types'
|
||||
import { getProducts, getCategories } from '../../api/product'
|
||||
import ProductCard from '../../components/ProductCard.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { Product, Category } from '../../types'
|
||||
import { getProducts, getCategories } from '../../api/product'
|
||||
import { BASE_URL } from '../../utils/request'
|
||||
import ProductCard from '../../components/ProductCard.vue'
|
||||
import CustomerServiceBtn from '../../components/CustomerServiceBtn.vue'
|
||||
|
||||
const products = ref<Product[]>([])
|
||||
const categories = ref<Category[]>([])
|
||||
const activeCategoryId = ref<number | undefined>(undefined)
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const products = ref<Product[]>([])
|
||||
const categories = ref<Category[]>([])
|
||||
const activeCategoryId = ref<number | undefined>(undefined)
|
||||
const loading = ref(false)
|
||||
const showQrcode = ref(false)
|
||||
const page = ref(1)
|
||||
const keyword = ref('')
|
||||
const pageSize = 20
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const data = await getCategories()
|
||||
categories.value = [{ id: 0, name: '全部', sort: 0 } as Category, ...data]
|
||||
} catch {
|
||||
// 错误已在 request 中统一处理
|
||||
}
|
||||
}
|
||||
// 自定义导航栏高度
|
||||
const statusBarHeight = ref(20)
|
||||
const navBarHeight = ref(44)
|
||||
|
||||
async function loadProducts(reset = false) {
|
||||
if (reset) {
|
||||
page.value = 1
|
||||
products.value = []
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const params: Record<string, unknown> = { page: page.value, pageSize }
|
||||
if (activeCategoryId.value && activeCategoryId.value !== 0) {
|
||||
params.categoryId = activeCategoryId.value
|
||||
}
|
||||
const data = await getProducts(params as { categoryId?: number; page?: number; pageSize?: number })
|
||||
if (reset) {
|
||||
products.value = data.list
|
||||
} else {
|
||||
products.value.push(...data.list)
|
||||
}
|
||||
} catch {
|
||||
// 错误已在 request 中统一处理
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
try {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 20
|
||||
// #ifdef MP-WEIXIN
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
navBarHeight.value = (menuBtn.top - (sysInfo.statusBarHeight || 20)) * 2 + menuBtn.height
|
||||
// #endif
|
||||
} catch { /* fallback */ }
|
||||
|
||||
function selectCategory(id: number) {
|
||||
activeCategoryId.value = id
|
||||
loadProducts(true)
|
||||
}
|
||||
function fullUrl(path : string) : string {
|
||||
if (!path) return ''
|
||||
if (path.startsWith('http')) return path
|
||||
return BASE_URL + path
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCategories()
|
||||
loadProducts(true)
|
||||
})
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const data = await getCategories()
|
||||
categories.value = data
|
||||
} catch { /* handled */ }
|
||||
}
|
||||
|
||||
async function loadProducts(reset = false) {
|
||||
if (reset) { page.value = 1; products.value = [] }
|
||||
loading.value = true
|
||||
try {
|
||||
const params : Record<string, unknown> = { page: page.value, pageSize }
|
||||
if (activeCategoryId.value) params.categoryId = activeCategoryId.value
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
const data = await getProducts(params as any)
|
||||
if (reset) { products.value = data.list } else { products.value.push(...data.list) }
|
||||
} catch { /* handled */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function selectCategory(id : number) {
|
||||
activeCategoryId.value = activeCategoryId.value === id ? undefined : id
|
||||
loadProducts(true)
|
||||
}
|
||||
|
||||
function goSearch() {
|
||||
// 可扩展为独立搜索页,目前直接触发搜索
|
||||
}
|
||||
|
||||
function goCalculator() {
|
||||
uni.navigateTo({ url: '/pages/calculator/index' })
|
||||
}
|
||||
|
||||
onMounted(() => { loadCategories(); loadProducts(true) })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.category-bar {
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
padding: 20rpx 16rpx;
|
||||
}
|
||||
.category-item {
|
||||
display: inline-block;
|
||||
padding: 12rpx 28rpx;
|
||||
margin-right: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
border-radius: 28rpx;
|
||||
}
|
||||
.category-item--active {
|
||||
color: #fff;
|
||||
background: #e4393c;
|
||||
}
|
||||
.product-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.product-grid__item {
|
||||
width: calc(50% - 8rpx);
|
||||
}
|
||||
.empty-tip,
|
||||
.loading-tip {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
background: linear-gradient(to right, #FFCFDE, #FFA6C4);
|
||||
}
|
||||
|
||||
.custom-navbar__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-navbar__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.search-bar__input {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 40rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
height: 72rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-bar__icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.search-bar__placeholder {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.search-bar__btn {
|
||||
margin-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 分类图标 */
|
||||
.category-section {
|
||||
white-space: nowrap;
|
||||
padding: 32rpx 0 24rpx;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.category-section__inner {
|
||||
display: inline-flex;
|
||||
padding: 0 24rpx;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon__circle {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 30rpx;
|
||||
background: linear-gradient(135deg, #fce4ec, #f8bbd0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.category-icon--active .category-icon__circle {
|
||||
background: linear-gradient(135deg, #f48fb1, #e91e63);
|
||||
box-shadow: 0 4rpx 16rpx rgba(233, 30, 99, 0.3);
|
||||
}
|
||||
|
||||
.category-icon__emoji {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.category-icon__img {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
}
|
||||
|
||||
.category-icon__label {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-icon--active .category-icon__label {
|
||||
color: #e91e63;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 快捷入口 */
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 0 24rpx 24rpx;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.quick-action {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx 0;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.quick-action--calc {
|
||||
background: linear-gradient(135deg, #FFA4C3, #FFD2E0);
|
||||
}
|
||||
|
||||
.quick-action--service {
|
||||
background: linear-gradient(135deg, #e8f5e9, #fff);
|
||||
}
|
||||
|
||||
.quick-action__icon {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
|
||||
.quick-action__text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 商品列表 */
|
||||
.product-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 24rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.product-grid__item {
|
||||
width: calc(50% - 8rpx);
|
||||
}
|
||||
|
||||
.empty-tip,
|
||||
.loading-tip {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,26 +1,42 @@
|
|||
<template>
|
||||
<view class="product-detail" v-if="product">
|
||||
<!-- Banner 轮播 -->
|
||||
<BannerSwiper :images="product.bannerImages" :video="product.bannerVideo" />
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="custom-navbar__content" :style="{ height: navBarHeight + 'px' }">
|
||||
<image class="custom-navbar__back" src="/static/ic_back.png" mode="aspectFit" @click="goBack" />
|
||||
<text class="custom-navbar__title">商品详情</text>
|
||||
<view class="custom-navbar__placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基础属性 -->
|
||||
<!-- 导航栏占位 -->
|
||||
<view :style="{ height: (statusBarHeight + navBarHeight) + 'px' }" />
|
||||
|
||||
<!-- Banner 轮播 -->
|
||||
<view class="banner-wrapper">
|
||||
<BannerSwiper :images="product.bannerImages || []" :video="product.bannerVideo" />
|
||||
</view>
|
||||
|
||||
<!-- 基础信息 -->
|
||||
<view class="base-info">
|
||||
<text class="base-info__price">¥{{ product.basePrice }}</text>
|
||||
<text class="base-info__name">{{ product.name }}</text>
|
||||
<view class="base-info__top">
|
||||
<text class="base-info__name">{{ product.name }}</text>
|
||||
<view class="base-info__price">
|
||||
<text class="base-info__price-symbol">¥</text>
|
||||
<text class="base-info__price-num">{{ product.basePrice }}</text>
|
||||
<text class="base-info__price-unit">元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="base-info__attrs">
|
||||
<view class="attr-row">
|
||||
<text class="attr-label">款号</text>
|
||||
<text class="attr-label">款 号</text>
|
||||
<text class="attr-value">{{ product.styleNo }}</text>
|
||||
</view>
|
||||
<view class="attr-row">
|
||||
<text class="attr-label">库存</text>
|
||||
<text class="attr-value">{{ product.stock }}</text>
|
||||
</view>
|
||||
<view class="attr-row">
|
||||
<text class="attr-label">损耗</text>
|
||||
<text class="attr-value">{{ product.loss }}</text>
|
||||
</view>
|
||||
<view class="attr-row">
|
||||
<text class="attr-label">损 耗</text>
|
||||
<text class="attr-value">{{ product.loss }}%</text>
|
||||
<text class="attr-label">工费</text>
|
||||
<text class="attr-value">¥{{ product.laborCost }}</text>
|
||||
</view>
|
||||
|
|
@ -30,25 +46,40 @@
|
|||
<!-- 发货公告 -->
|
||||
<ShippingNotice />
|
||||
|
||||
<!-- 查看详细参数入口 -->
|
||||
<view class="spec-entry" @click="showSpecPanel = true">
|
||||
<text>查看详细参数</text>
|
||||
<text class="spec-entry__arrow">›</text>
|
||||
<!-- 商品详情(大图展示) -->
|
||||
<view class="detail-section">
|
||||
<view class="detail-section__title">商品详情</view>
|
||||
<view class="detail-section__images">
|
||||
<image
|
||||
v-for="(img, idx) in (product.bannerImages || [])"
|
||||
:key="idx"
|
||||
class="detail-section__img"
|
||||
:src="fullUrl(img)"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详细参数面板 -->
|
||||
<SpecPanel
|
||||
v-if="showSpecPanel"
|
||||
:product-id="product.id"
|
||||
@close="showSpecPanel = false"
|
||||
/>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="bottom-bar__btn bottom-bar__btn--cs" @click="showQrCode = true">客服</view>
|
||||
<view class="bottom-bar__btn bottom-bar__btn--cart" @click="handleAddToCart">加入购物车</view>
|
||||
<view class="bottom-bar__icons">
|
||||
<view class="bottom-bar__icon-item" @click="showQrCode = true">
|
||||
<image class="bottom-bar__icon-img" src="/static/ic_customer.png" mode="aspectFit" />
|
||||
<text class="bottom-bar__icon-text">客服</text>
|
||||
</view>
|
||||
<view class="bottom-bar__icon-item" @click="goCart">
|
||||
<image class="bottom-bar__icon-img" src="/static/tab/car.png" mode="aspectFit" />
|
||||
<text class="bottom-bar__icon-text">购物车</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-bar__main-btn" @click="showSpecPanel = true">
|
||||
<text>空托—查看详细参数</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详细参数面板 -->
|
||||
<SpecPanel v-if="showSpecPanel" :product-id="product.id" @close="showSpecPanel = false" />
|
||||
|
||||
<!-- 客服二维码弹窗 -->
|
||||
<CustomerServiceBtn v-if="showQrCode" mode="qrcode" @close="showQrCode = false" />
|
||||
</view>
|
||||
|
|
@ -56,9 +87,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { Product, CartItem } from '../../types'
|
||||
import type { Product } from '../../types'
|
||||
import { getProductDetail } from '../../api/product'
|
||||
import { useCartStore } from '../../store/cart'
|
||||
import { BASE_URL } from '../../utils/request'
|
||||
import BannerSwiper from '../../components/BannerSwiper.vue'
|
||||
import ShippingNotice from '../../components/ShippingNotice.vue'
|
||||
import SpecPanel from '../../components/SpecPanel.vue'
|
||||
|
|
@ -67,15 +98,37 @@ import CustomerServiceBtn from '../../components/CustomerServiceBtn.vue'
|
|||
const product = ref<Product | null>(null)
|
||||
const showSpecPanel = ref(false)
|
||||
const showQrCode = ref(false)
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const statusBarHeight = ref(20)
|
||||
const navBarHeight = ref(44)
|
||||
try {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 20
|
||||
// #ifdef MP-WEIXIN
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
navBarHeight.value = (menuBtn.top - (sysInfo.statusBarHeight || 20)) * 2 + menuBtn.height
|
||||
// #endif
|
||||
} catch { /* fallback */ }
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
}
|
||||
|
||||
function fullUrl(path: string): string {
|
||||
if (!path) return ''
|
||||
if (path.startsWith('http')) return path
|
||||
return BASE_URL + path
|
||||
}
|
||||
|
||||
function goCart() {
|
||||
uni.switchTab({ url: '/pages/cart/index' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as { options?: { id?: string } }
|
||||
const id = Number(currentPage.options?.id)
|
||||
if (id) {
|
||||
loadProduct(id)
|
||||
}
|
||||
if (id) loadProduct(id)
|
||||
})
|
||||
|
||||
async function loadProduct(id: number) {
|
||||
|
|
@ -85,101 +138,140 @@ async function loadProduct(id: number) {
|
|||
uni.showToast({ title: '加载商品失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddToCart() {
|
||||
if (!product.value) return
|
||||
const item: CartItem = {
|
||||
id: Date.now(),
|
||||
userId: 0,
|
||||
productId: product.value.id,
|
||||
specDataId: 0,
|
||||
quantity: 1,
|
||||
product: product.value,
|
||||
specData: {
|
||||
id: 0,
|
||||
productId: product.value.id,
|
||||
modelName: product.value.name,
|
||||
fineness: '',
|
||||
mainStone: '',
|
||||
ringSize: '',
|
||||
goldTotalWeight: 0,
|
||||
goldNetWeight: 0,
|
||||
loss: 0,
|
||||
goldLoss: 0,
|
||||
goldPrice: 0,
|
||||
goldValue: 0,
|
||||
mainStoneCount: 0,
|
||||
mainStoneWeight: 0,
|
||||
mainStoneUnitPrice: 0,
|
||||
mainStoneAmount: 0,
|
||||
sideStoneCount: 0,
|
||||
sideStoneWeight: 0,
|
||||
sideStoneUnitPrice: 0,
|
||||
sideStoneAmount: 0,
|
||||
accessoryAmount: 0,
|
||||
processingFee: 0,
|
||||
settingFee: 0,
|
||||
totalLaborCost: 0,
|
||||
totalPrice: product.value.basePrice,
|
||||
},
|
||||
checked: true,
|
||||
}
|
||||
cartStore.addToCart(item)
|
||||
uni.showToast({ title: '已加入购物车', icon: 'success' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-detail {
|
||||
padding-bottom: 120rpx;
|
||||
padding-bottom: 140rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
background: linear-gradient(to right, #FFCFDE, #FFA6C4);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.custom-navbar__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
.custom-navbar__back {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
.custom-navbar__title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.custom-navbar__placeholder {
|
||||
width: 44rpx;
|
||||
}
|
||||
|
||||
/* Banner 容器 */
|
||||
.banner-wrapper {
|
||||
margin: 20rpx 24rpx 0;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 基础信息卡片 */
|
||||
.base-info {
|
||||
background: #fff;
|
||||
padding: 24rpx;
|
||||
margin: 20rpx 24rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.base-info__price {
|
||||
font-size: 40rpx;
|
||||
color: #e4393c;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
.base-info__top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.base-info__name {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-top: 12rpx;
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.base-info__price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: #FF6D9B;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.base-info__price-symbol {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.base-info__price-num {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.base-info__price-unit {
|
||||
font-size: 24rpx;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
/* 属性行 - 两列布局 */
|
||||
.base-info__attrs {
|
||||
margin-top: 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
.attr-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
.attr-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
width: 80rpx;
|
||||
flex-shrink: 0;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
.attr-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
.spec-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
margin-top: 16rpx;
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
min-width: 180rpx;
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
.spec-entry__arrow {
|
||||
color: #999;
|
||||
font-size: 32rpx;
|
||||
|
||||
/* 商品详情大图 */
|
||||
.detail-section {
|
||||
background: #fff;
|
||||
margin: 20rpx 24rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.detail-section__title {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.detail-section__images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.detail-section__img {
|
||||
width: 100%;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
|
@ -187,25 +279,39 @@ function handleAddToCart() {
|
|||
right: 0;
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.bottom-bar__btn {
|
||||
.bottom-bar__icons {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bottom-bar__icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.bottom-bar__icon-img {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
.bottom-bar__icon-text {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
.bottom-bar__main-btn {
|
||||
flex: 1;
|
||||
background: linear-gradient(to right, #f5a0b8, #e4393c);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
padding: 24rpx 0;
|
||||
border-radius: 44rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.bottom-bar__btn--cs {
|
||||
flex: 1;
|
||||
border: 1rpx solid #ddd;
|
||||
color: #666;
|
||||
background: #fff;
|
||||
}
|
||||
.bottom-bar__btn--cart {
|
||||
flex: 2;
|
||||
background: #e4393c;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
BIN
miniprogram/static/ic_back.png
Normal file
BIN
miniprogram/static/ic_back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 648 B |
BIN
miniprogram/static/ic_notice.png
Normal file
BIN
miniprogram/static/ic_notice.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
miniprogram/static/ic_search.png
Normal file
BIN
miniprogram/static/ic_search.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,22 +1,19 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { CartItem } from '../types'
|
||||
import { getCartList, addToCart as apiAddToCart, deleteCartItem as apiDeleteCartItem, updateCartItem as apiUpdateCartItem } from '../api/cart'
|
||||
|
||||
export const useCartStore = defineStore('cart', () => {
|
||||
const items = ref<CartItem[]>([])
|
||||
|
||||
/** 已勾选的商品 */
|
||||
const checkedItems = computed(() => items.value.filter((item) => item.checked))
|
||||
|
||||
/** 已勾选商品总金额 */
|
||||
const totalAmount = computed(() =>
|
||||
checkedItems.value.reduce((sum, item) => sum + item.specData.totalPrice * item.quantity, 0),
|
||||
)
|
||||
|
||||
/** 从后端拉取购物车列表并同步到本地 */
|
||||
async function fetchCart() {
|
||||
try {
|
||||
const { getCartList } = await import('../api/cart')
|
||||
const list = await getCartList()
|
||||
items.value = list.map((item) => ({ ...item, checked: true }))
|
||||
} catch {
|
||||
|
|
@ -24,38 +21,28 @@ export const useCartStore = defineStore('cart', () => {
|
|||
}
|
||||
}
|
||||
|
||||
/** 添加商品到购物车(本地优先,后台同步) */
|
||||
function addToCart(item: CartItem) {
|
||||
items.value.push(item)
|
||||
// 异步同步到后端
|
||||
import('../api/cart').then(({ addToCart: apiAdd }) => {
|
||||
apiAdd({
|
||||
productId: item.productId,
|
||||
specDataId: item.specDataId,
|
||||
quantity: item.quantity,
|
||||
}).catch(() => { /* 静默处理 */ })
|
||||
apiAddToCart({
|
||||
productId: item.productId,
|
||||
specDataId: item.specDataId,
|
||||
quantity: item.quantity,
|
||||
}).catch(() => { /* 静默处理 */ })
|
||||
}
|
||||
|
||||
/** 移除购物车项 */
|
||||
function removeFromCart(id: number) {
|
||||
const index = items.value.findIndex((item) => item.id === id)
|
||||
if (index !== -1) {
|
||||
items.value.splice(index, 1)
|
||||
import('../api/cart').then(({ deleteCartItem }) => {
|
||||
deleteCartItem(id).catch(() => { /* 静默处理 */ })
|
||||
}).catch(() => { /* 静默处理 */ })
|
||||
apiDeleteCartItem(id).catch(() => { /* 静默处理 */ })
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新数量 */
|
||||
function updateQuantity(id: number, quantity: number) {
|
||||
const item = items.value.find((item) => item.id === id)
|
||||
if (item) {
|
||||
item.quantity = quantity
|
||||
import('../api/cart').then(({ updateCartItem }) => {
|
||||
updateCartItem(id, { quantity }).catch(() => { /* 静默处理 */ })
|
||||
}).catch(() => { /* 静默处理 */ })
|
||||
apiUpdateCartItem(id, { quantity }).catch(() => { /* 静默处理 */ })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface Product {
|
|||
categoryId: number
|
||||
bannerImages: string[]
|
||||
bannerVideo?: string
|
||||
thumb?: string
|
||||
status: 'on' | 'off'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
|
|
@ -20,6 +21,7 @@ export interface Product {
|
|||
export interface Category {
|
||||
id: number
|
||||
name: string
|
||||
icon?: string
|
||||
parentId?: number
|
||||
sort: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"cart.js","sources":["api/cart.ts"],"sourcesContent":["import { get, post, put, del } from '../utils/request'\r\nimport type { CartItem } from '../types'\r\n\r\n/** 获取购物车列表 */\r\nexport const getCartList = () =>\r\n get<CartItem[]>('/api/cart')\r\n\r\n/** 添加商品到购物车 */\r\nexport const addToCart = (data: { productId: number; specDataId: number; quantity: number }) =>\r\n post<CartItem>('/api/cart', data as unknown as Record<string, unknown>)\r\n\r\n/** 更新购物车项 */\r\nexport const updateCartItem = (id: number, data: { quantity: number }) =>\r\n put<CartItem>(`/api/cart/${id}`, data as unknown as Record<string, unknown>)\r\n\r\n/** 删除购物车项 */\r\nexport const deleteCartItem = (id: number) =>\r\n del(`/api/cart/${id}`)\r\n"],"names":["get","post","put","del"],"mappings":";;;AAIa,MAAA,cAAc,MACzBA,cAAA,IAAgB,WAAW;AAGtB,MAAM,YAAY,CAAC,SACxBC,mBAAe,aAAa,IAA0C;AAG3D,MAAA,iBAAiB,CAAC,IAAY,SACzCC,cAAAA,IAAc,aAAa,EAAE,IAAI,IAA0C;AAGtE,MAAM,iBAAiB,CAAC,OAC7BC,cAAAA,IAAI,aAAa,EAAE,EAAE;;;;;"}
|
||||
{"version":3,"file":"cart.js","sources":["api/cart.ts"],"sourcesContent":["import { get, post, put, del } from '../utils/request'\r\nimport type { CartItem } from '../types'\r\n\r\n/** 获取购物车列表 */\r\nexport const getCartList = () =>\r\n get<CartItem[]>('/api/cart')\r\n\r\n/** 添加商品到购物车 */\r\nexport const addToCart = (data: { productId: number; specDataId: number; quantity: number }) =>\r\n post<CartItem>('/api/cart', data as unknown as Record<string, unknown>)\r\n\r\n/** 更新购物车项 */\r\nexport const updateCartItem = (id: number, data: { quantity: number }) =>\r\n put<CartItem>(`/api/cart/${id}`, data as unknown as Record<string, unknown>)\r\n\r\n/** 删除购物车项 */\r\nexport const deleteCartItem = (id: number) =>\r\n del(`/api/cart/${id}`)\r\n"],"names":["get","post","put","del"],"mappings":";;AAIa,MAAA,cAAc,MACzBA,cAAA,IAAgB,WAAW;AAGtB,MAAM,YAAY,CAAC,SACxBC,mBAAe,aAAa,IAA0C;AAG3D,MAAA,iBAAiB,CAAC,IAAY,SACzCC,cAAAA,IAAc,aAAa,EAAE,IAAI,IAA0C;AAGtE,MAAM,iBAAiB,CAAC,OAC7BC,cAAAA,IAAI,aAAa,EAAE,EAAE;;;;;"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"product.js","sources":["api/product.ts"],"sourcesContent":["import { get, post } from '../utils/request'\r\nimport type { Product, Category, DetailParameterConfig, SpecData } from '../types'\r\n\r\n/** 获取商品列表 */\r\nexport const getProducts = (params?: { categoryId?: number; page?: number; pageSize?: number }) =>\r\n get<{ list: Product[]; total: number }>('/api/products', params as Record<string, unknown>)\r\n\r\n/** 获取商品详情 */\r\nexport const getProductDetail = (id: number) =>\r\n get<Product>(`/api/products/${id}`)\r\n\r\n/** 获取商品详细参数选项 */\r\nexport const getProductSpecs = (id: number) =>\r\n get<DetailParameterConfig>(`/api/products/${id}/specs`)\r\n\r\n/** 根据参数组合获取规格数据列表 */\r\nexport const getSpecDataList = (id: number, params: { fineness: string; mainStone: string; ringSize: string }) =>\r\n post<SpecData[]>(`/api/products/${id}/spec-data`, params as unknown as Record<string, unknown>)\r\n\r\n/** 获取商品分类列表 */\r\nexport const getCategories = () =>\r\n get<Category[]>('/api/categories')\r\n"],"names":["get","post"],"mappings":";;AAIO,MAAM,cAAc,CAAC,WAC1BA,kBAAwC,iBAAiB,MAAiC;AAGrF,MAAM,mBAAmB,CAAC,OAC/BA,cAAAA,IAAa,iBAAiB,EAAE,EAAE;AAG7B,MAAM,kBAAkB,CAAC,OAC9BA,cAA2B,IAAA,iBAAiB,EAAE,QAAQ;AAG3C,MAAA,kBAAkB,CAAC,IAAY,WAC1CC,mBAAiB,iBAAiB,EAAE,cAAc,MAA4C;AAGnF,MAAA,gBAAgB,MAC3BD,cAAAA,IAAgB,iBAAiB;;;;;;"}
|
||||
{"version":3,"file":"product.js","sources":["api/product.ts"],"sourcesContent":["import { get, post } from '../utils/request'\r\nimport type { Product, Category, DetailParameterConfig, SpecData } from '../types'\r\n\r\n/** 获取商品列表 */\r\nexport const getProducts = (params?: { categoryId?: number; page?: number; pageSize?: number }) =>\r\n get<{ list: Product[]; total: number }>('/api/products', params as Record<string, unknown>)\r\n\r\n/** 获取商品详情 */\r\nexport const getProductDetail = (id: number) =>\r\n get<Product>(`/api/products/${id}`)\r\n\r\n/** 获取商品详细参数选项 */\r\nexport const getProductSpecs = (id: number) =>\r\n get<DetailParameterConfig>(`/api/products/${id}/specs`)\r\n\r\n/** 根据参数组合获取规格数据列表 */\r\nexport const getSpecDataList = (id: number, params: { fineness?: string; mainStone?: string; ringSize?: string }) =>\r\n post<SpecData[]>(`/api/products/${id}/spec-data`, params as unknown as Record<string, unknown>)\r\n\r\n/** 获取商品分类列表 */\r\nexport const getCategories = () =>\r\n get<Category[]>('/api/categories')\r\n"],"names":["get","post"],"mappings":";;AAIO,MAAM,cAAc,CAAC,WAC1BA,kBAAwC,iBAAiB,MAAiC;AAGrF,MAAM,mBAAmB,CAAC,OAC/BA,cAAAA,IAAa,iBAAiB,EAAE,EAAE;AAG7B,MAAM,kBAAkB,CAAC,OAC9BA,cAA2B,IAAA,iBAAiB,EAAE,QAAQ;AAG3C,MAAA,kBAAkB,CAAC,IAAY,WAC1CC,mBAAiB,iBAAiB,EAAE,cAAc,MAA4C;AAGnF,MAAA,gBAAgB,MAC3BD,cAAAA,IAAgB,iBAAiB;;;;;;"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"assets.js","sources":["static/tab/me_s.png","static/ic_address.png","static/ic_customer.png","static/ic_about.png","static/ic_agreement1.png","static/ic_agreement2.png","static/logo.png"],"sourcesContent":["export default \"__VITE_ASSET__0724ec6f__\"","export default \"__VITE_ASSET__2fa96069__\"","export default \"__VITE_ASSET__834c867b__\"","export default \"__VITE_ASSET__53f234dc__\"","export default \"__VITE_ASSET__a736f8f4__\"","export default \"__VITE_ASSET__a25b5e1b__\"","export default \"__VITE_ASSET__46719607__\""],"names":[],"mappings":";AAAA,MAAe,eAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;;;;;;;;"}
|
||||
{"version":3,"file":"assets.js","sources":["static/ic_search.png","static/ic_back.png","static/ic_customer.png","static/tab/car.png","static/tab/me_s.png","static/ic_address.png","static/ic_about.png","static/ic_agreement1.png","static/ic_agreement2.png","static/logo.png","static/ic_notice.png"],"sourcesContent":["export default \"__VITE_ASSET__cb299f1c__\"","export default \"__VITE_ASSET__a09e4f4f__\"","export default \"__VITE_ASSET__834c867b__\"","export default \"__VITE_ASSET__de453aec__\"","export default \"__VITE_ASSET__0724ec6f__\"","export default \"__VITE_ASSET__2fa96069__\"","export default \"__VITE_ASSET__53f234dc__\"","export default \"__VITE_ASSET__a736f8f4__\"","export default \"__VITE_ASSET__a25b5e1b__\"","export default \"__VITE_ASSET__46719607__\"","export default \"__VITE_ASSET__abe1da0c__\""],"names":[],"mappings":";AAAA,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,aAAA;ACAf,MAAe,eAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,eAAA;ACAf,MAAe,aAAA;;;;;;;;;;;;"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"BannerSwiper.js","sources":["../../../../Software/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/RjovZ2l0Q29kZS91bmlhcHAvSmV3ZWxyeU1hbGwvbWluaXByb2dyYW0vY29tcG9uZW50cy9CYW5uZXJTd2lwZXIudnVl"],"sourcesContent":["import Component from 'F:/gitCode/uniapp/JewelryMall/miniprogram/components/BannerSwiper.vue'\nwx.createComponent(Component)"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AACA,GAAG,gBAAgB,SAAS;"}
|
||||
{"version":3,"file":"BannerSwiper.js","sources":["components/BannerSwiper.vue","../../../../Software/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/RjovZ2l0Q29kZS91bmlhcHAvSmV3ZWxyeU1hbGwvbWluaXByb2dyYW0vY29tcG9uZW50cy9CYW5uZXJTd2lwZXIudnVl"],"sourcesContent":["<template>\r\n <swiper class=\"banner-swiper\" :indicator-dots=\"true\" :autoplay=\"true\" :interval=\"3000\" circular>\r\n <swiper-item v-if=\"video\">\r\n <video class=\"banner-swiper__video\" :src=\"fullUrl(video)\" controls />\r\n </swiper-item>\r\n <swiper-item v-for=\"(img, idx) in images\" :key=\"idx\">\r\n <image class=\"banner-swiper__image\" :src=\"fullUrl(img)\" mode=\"aspectFill\" />\r\n </swiper-item>\r\n </swiper>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { BASE_URL } from '../utils/request'\r\n\r\ndefineProps<{\r\n images: string[]\r\n video?: string\r\n}>()\r\n\r\nfunction fullUrl(path: string): string {\r\n if (!path) return ''\r\n if (path.startsWith('http')) return path\r\n return BASE_URL + path\r\n}\r\n</script>\r\n\r\n<style scoped>\r\n.banner-swiper { width: 100%; height: 600rpx; }\r\n.banner-swiper__image { width: 100%; height: 600rpx; }\r\n.banner-swiper__video { width: 100%; height: 600rpx; }\r\n</style>\r\n","import Component from 'F:/gitCode/uniapp/JewelryMall/miniprogram/components/BannerSwiper.vue'\nwx.createComponent(Component)"],"names":["BASE_URL"],"mappings":";;;;;;;;;;AAmBA,aAAS,QAAQ,MAAsB;AACrC,UAAI,CAAC;AAAa,eAAA;AACd,UAAA,KAAK,WAAW,MAAM;AAAU,eAAA;AACpC,aAAOA,cAAAA,WAAW;AAAA,IACpB;;;;;;;;;;;;;;;;;;ACtBA,GAAG,gBAAgB,SAAS;"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"ProductCard.js","sources":["components/ProductCard.vue","../../../../Software/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/RjovZ2l0Q29kZS91bmlhcHAvSmV3ZWxyeU1hbGwvbWluaXByb2dyYW0vY29tcG9uZW50cy9Qcm9kdWN0Q2FyZC52dWU"],"sourcesContent":["<template>\r\n <view class=\"product-card\" @click=\"goDetail\">\r\n <image\r\n class=\"product-card__image\"\r\n :src=\"product.bannerImages[0] || '/static/logo.png'\"\r\n mode=\"aspectFill\"\r\n />\r\n <view class=\"product-card__info\">\r\n <text class=\"product-card__name\">{{ product.name }}</text>\r\n <text class=\"product-card__style\">款号:{{ product.styleNo }}</text>\r\n <view class=\"product-card__bottom\">\r\n <text class=\"product-card__price\">¥{{ product.basePrice }}</text>\r\n <text class=\"product-card__stock\">库存 {{ product.stock }}</text>\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { Product } from '../types'\r\n\r\nconst props = defineProps<{\r\n product: Product\r\n}>()\r\n\r\nfunction goDetail() {\r\n uni.navigateTo({ url: `/pages/product/detail?id=${props.product.id}` })\r\n}\r\n</script>\r\n\r\n<style scoped>\r\n.product-card {\r\n display: flex;\r\n flex-direction: column;\r\n background: #fff;\r\n border-radius: 12rpx;\r\n overflow: hidden;\r\n width: 100%;\r\n}\r\n.product-card__image {\r\n width: 100%;\r\n height: 340rpx;\r\n}\r\n.product-card__info {\r\n padding: 16rpx;\r\n}\r\n.product-card__name {\r\n font-size: 28rpx;\r\n color: #333;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n display: block;\r\n}\r\n.product-card__style {\r\n font-size: 22rpx;\r\n color: #999;\r\n margin-top: 8rpx;\r\n display: block;\r\n}\r\n.product-card__bottom {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-top: 12rpx;\r\n}\r\n.product-card__price {\r\n font-size: 32rpx;\r\n color: #e4393c;\r\n font-weight: bold;\r\n}\r\n.product-card__stock {\r\n font-size: 22rpx;\r\n color: #999;\r\n}\r\n</style>\r\n","import Component from 'F:/gitCode/uniapp/JewelryMall/miniprogram/components/ProductCard.vue'\nwx.createComponent(Component)"],"names":["uni"],"mappings":";;;;;;;;AAqBA,UAAM,QAAQ;AAId,aAAS,WAAW;AACdA,0BAAA,WAAW,EAAE,KAAK,4BAA4B,MAAM,QAAQ,EAAE,IAAI;AAAA,IACxE;;;;;;;;;;;;;;AC1BA,GAAG,gBAAgB,SAAS;"}
|
||||
{"version":3,"file":"ProductCard.js","sources":["components/ProductCard.vue","../../../../Software/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/RjovZ2l0Q29kZS91bmlhcHAvSmV3ZWxyeU1hbGwvbWluaXByb2dyYW0vY29tcG9uZW50cy9Qcm9kdWN0Q2FyZC52dWU"],"sourcesContent":["<template>\r\n\t<view class=\"product-card\" @click=\"goDetail\">\r\n\t\t<image class=\"product-card__image\" :src=\"imgSrc()\" mode=\"aspectFill\" />\r\n\t\t<view class=\"product-card__info\">\r\n\t\t\t<text class=\"product-card__name\">{{ product.name }}({{ product.styleNo }})</text>\r\n\t\t\t<view class=\"product-card__bottom\">\r\n\t\t\t\t<view class=\"product-card__price-tag\">\r\n\t\t\t\t\t<text class=\"product-card__price\">¥{{ product.basePrice }}</text>\r\n\t\t\t\t</view>\r\n\t\t\t\t<text class=\"product-card__stock\">库存{{ product.stock }}</text>\r\n\t\t\t</view>\r\n\t\t</view>\r\n\t</view>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n\timport type { Product } from '../types'\r\n\timport { BASE_URL } from '../utils/request'\r\n\r\n\tconst props = defineProps<{ product : Product }>()\r\n\r\n\tfunction imgSrc() : string {\r\n\t\tconst url = props.product.thumb || props.product.bannerImages?.[0]\r\n\t\tif (!url) return '/static/logo.png'\r\n\t\tif (url.startsWith('http')) return url\r\n\t\treturn BASE_URL + url\r\n\t}\r\n\tfunction goDetail() {\r\n\t\tuni.navigateTo({ url: `/pages/product/detail?id=${props.product.id}` })\r\n\t}\r\n</script>\r\n\r\n<style scoped>\r\n\t.product-card {\r\n\t\tdisplay: flex;\r\n\t\tflex-direction: column;\r\n\t\tbackground: #fff;\r\n\t\tborder-radius: 16rpx;\r\n\t\toverflow: hidden;\r\n\t\twidth: 100%;\r\n\t}\r\n\r\n\t.product-card__image {\r\n\t\twidth: 100%;\r\n\t\theight: 340rpx;\r\n\t}\r\n\r\n\t.product-card__info {\r\n\t\tpadding: 16rpx 20rpx 20rpx;\r\n\t}\r\n\r\n\t.product-card__name {\r\n\t\tfont-size: 26rpx;\r\n\t\tcolor: #333;\r\n\t\toverflow: hidden;\r\n\t\ttext-overflow: ellipsis;\r\n\t\twhite-space: nowrap;\r\n\t\tdisplay: block;\r\n\t\tline-height: 1.5;\r\n\t}\r\n\r\n\t.product-card__bottom {\r\n\t\tdisplay: flex;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t\tmargin-top: 12rpx;\r\n\t}\r\n\r\n\t.product-card__price-tag {\r\n\t\tbackground: linear-gradient(135deg, #f5a0b8, #FF6D9B);\r\n\t\tborder-radius: 8rpx;\r\n\t\tpadding: 4rpx 16rpx;\r\n\t}\r\n\r\n\t.product-card__price {\r\n\t\tfont-size: 28rpx;\r\n\t\tcolor: #fff;\r\n\t\tfont-weight: bold;\r\n\t}\r\n\r\n\t.product-card__stock {\r\n\t\tfont-size: 22rpx;\r\n\t\tcolor: #999;\r\n\t}\r\n</style>","import Component from 'F:/gitCode/uniapp/JewelryMall/miniprogram/components/ProductCard.vue'\nwx.createComponent(Component)"],"names":["BASE_URL","uni"],"mappings":";;;;;;;;;AAmBC,UAAM,QAAQ;AAEd,aAAS,SAAkB;;AAC1B,YAAM,MAAM,MAAM,QAAQ,WAAS,WAAM,QAAQ,iBAAd,mBAA6B;AAChE,UAAI,CAAC;AAAY,eAAA;AACb,UAAA,IAAI,WAAW,MAAM;AAAU,eAAA;AACnC,aAAOA,cAAAA,WAAW;AAAA,IACnB;AACA,aAAS,WAAW;AACfC,0BAAA,WAAW,EAAE,KAAK,4BAA4B,MAAM,QAAQ,EAAE,IAAI;AAAA,IACvE;;;;;;;;;;;;;;AC5BD,GAAG,gBAAgB,SAAS;"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"ShippingNotice.js","sources":["../../../../Software/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/RjovZ2l0Q29kZS91bmlhcHAvSmV3ZWxyeU1hbGwvbWluaXByb2dyYW0vY29tcG9uZW50cy9TaGlwcGluZ05vdGljZS52dWU"],"sourcesContent":["import Component from 'F:/gitCode/uniapp/JewelryMall/miniprogram/components/ShippingNotice.vue'\nwx.createComponent(Component)"],"names":[],"mappings":";;;;;;;AACA,GAAG,gBAAgB,SAAS;"}
|
||||
{"version":3,"file":"ShippingNotice.js","sources":["../../../../Software/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/RjovZ2l0Q29kZS91bmlhcHAvSmV3ZWxyeU1hbGwvbWluaXByb2dyYW0vY29tcG9uZW50cy9TaGlwcGluZ05vdGljZS52dWU"],"sourcesContent":["import Component from 'F:/gitCode/uniapp/JewelryMall/miniprogram/components/ShippingNotice.vue'\nwx.createComponent(Component)"],"names":[],"mappings":";;;;;;;;;;AACA,GAAG,gBAAgB,SAAS;"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"cart.js","sources":["store/cart.ts"],"sourcesContent":["import { defineStore } from 'pinia'\r\nimport { ref, computed } from 'vue'\r\nimport type { CartItem } from '../types'\r\n\r\nexport const useCartStore = defineStore('cart', () => {\r\n const items = ref<CartItem[]>([])\r\n\r\n /** 已勾选的商品 */\r\n const checkedItems = computed(() => items.value.filter((item) => item.checked))\r\n\r\n /** 已勾选商品总金额 */\r\n const totalAmount = computed(() =>\r\n checkedItems.value.reduce((sum, item) => sum + item.specData.totalPrice * item.quantity, 0),\r\n )\r\n\r\n /** 从后端拉取购物车列表并同步到本地 */\r\n async function fetchCart() {\r\n try {\r\n const { getCartList } = await import('../api/cart')\r\n const list = await getCartList()\r\n items.value = list.map((item) => ({ ...item, checked: true }))\r\n } catch {\r\n // 未登录或网络异常,保留本地数据\r\n }\r\n }\r\n\r\n /** 添加商品到购物车(本地优先,后台同步) */\r\n function addToCart(item: CartItem) {\r\n items.value.push(item)\r\n // 异步同步到后端\r\n import('../api/cart').then(({ addToCart: apiAdd }) => {\r\n apiAdd({\r\n productId: item.productId,\r\n specDataId: item.specDataId,\r\n quantity: item.quantity,\r\n }).catch(() => { /* 静默处理 */ })\r\n }).catch(() => { /* 静默处理 */ })\r\n }\r\n\r\n /** 移除购物车项 */\r\n function removeFromCart(id: number) {\r\n const index = items.value.findIndex((item) => item.id === id)\r\n if (index !== -1) {\r\n items.value.splice(index, 1)\r\n import('../api/cart').then(({ deleteCartItem }) => {\r\n deleteCartItem(id).catch(() => { /* 静默处理 */ })\r\n }).catch(() => { /* 静默处理 */ })\r\n }\r\n }\r\n\r\n /** 更新数量 */\r\n function updateQuantity(id: number, quantity: number) {\r\n const item = items.value.find((item) => item.id === id)\r\n if (item) {\r\n item.quantity = quantity\r\n import('../api/cart').then(({ updateCartItem }) => {\r\n updateCartItem(id, { quantity }).catch(() => { /* 静默处理 */ })\r\n }).catch(() => { /* 静默处理 */ })\r\n }\r\n }\r\n\r\n /** 切换勾选状态(纯本地操作) */\r\n function toggleCheck(id: number) {\r\n const item = items.value.find((item) => item.id === id)\r\n if (item) {\r\n item.checked = !item.checked\r\n }\r\n }\r\n\r\n /** 全选/取消全选(纯本地操作) */\r\n function toggleCheckAll() {\r\n const allChecked = items.value.every((item) => item.checked)\r\n items.value.forEach((item) => {\r\n item.checked = !allChecked\r\n })\r\n }\r\n\r\n return {\r\n items,\r\n checkedItems,\r\n totalAmount,\r\n fetchCart,\r\n addToCart,\r\n removeFromCart,\r\n updateQuantity,\r\n toggleCheck,\r\n toggleCheckAll,\r\n }\r\n})\r\n"],"names":["defineStore","ref","computed","item"],"mappings":";;AAIa,MAAA,eAAeA,cAAAA,YAAY,QAAQ,MAAM;AAC9C,QAAA,QAAQC,kBAAgB,CAAA,CAAE;AAG1B,QAAA,eAAeC,cAAAA,SAAS,MAAM,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,OAAO,CAAC;AAG9E,QAAM,cAAcA,cAAA;AAAA,IAAS,MAC3B,aAAa,MAAM,OAAO,CAAC,KAAK,SAAS,MAAM,KAAK,SAAS,aAAa,KAAK,UAAU,CAAC;AAAA,EAAA;AAI5F,iBAAe,YAAY;AACrB,QAAA;AACF,YAAM,EAAE,YAAA,IAAgB,MAAa;AAC/B,YAAA,OAAO,MAAM;AACb,YAAA,QAAQ,KAAK,IAAI,CAAC,UAAU,EAAE,GAAG,MAAM,SAAS,KAAA,EAAO;AAAA,IAAA,QACvD;AAAA,IAER;AAAA,EACF;AAGA,WAAS,UAAU,MAAgB;AAC3B,UAAA,MAAM,KAAK,IAAI;AAErB,IAAO,iBAAe,KAAK,CAAC,EAAE,WAAW,aAAa;AAC7C,aAAA;AAAA,QACL,WAAW,KAAK;AAAA,QAChB,YAAY,KAAK;AAAA,QACjB,UAAU,KAAK;AAAA,MAAA,CAChB,EAAE,MAAM,MAAM;AAAA,MAAA,CAAc;AAAA,IAAA,CAC9B,EAAE,MAAM,MAAM;AAAA,IAAA,CAAc;AAAA,EAC/B;AAGA,WAAS,eAAe,IAAY;AAC5B,UAAA,QAAQ,MAAM,MAAM,UAAU,CAAC,SAAS,KAAK,OAAO,EAAE;AAC5D,QAAI,UAAU,IAAI;AACV,YAAA,MAAM,OAAO,OAAO,CAAC;AAC3B,MAAO,iBAAe,KAAK,CAAC,EAAE,qBAAqB;AAClC,uBAAA,EAAE,EAAE,MAAM,MAAM;AAAA,QAAA,CAAc;AAAA,MAAA,CAC9C,EAAE,MAAM,MAAM;AAAA,MAAA,CAAc;AAAA,IAC/B;AAAA,EACF;AAGS,WAAA,eAAe,IAAY,UAAkB;AAC9C,UAAA,OAAO,MAAM,MAAM,KAAK,CAACC,UAASA,MAAK,OAAO,EAAE;AACtD,QAAI,MAAM;AACR,WAAK,WAAW;AAChB,MAAO,iBAAe,KAAK,CAAC,EAAE,qBAAqB;AACjD,uBAAe,IAAI,EAAE,SAAA,CAAU,EAAE,MAAM,MAAM;AAAA,QAAA,CAAc;AAAA,MAAA,CAC5D,EAAE,MAAM,MAAM;AAAA,MAAA,CAAc;AAAA,IAC/B;AAAA,EACF;AAGA,WAAS,YAAY,IAAY;AACzB,UAAA,OAAO,MAAM,MAAM,KAAK,CAACA,UAASA,MAAK,OAAO,EAAE;AACtD,QAAI,MAAM;AACH,WAAA,UAAU,CAAC,KAAK;AAAA,IACvB;AAAA,EACF;AAGA,WAAS,iBAAiB;AACxB,UAAM,aAAa,MAAM,MAAM,MAAM,CAAC,SAAS,KAAK,OAAO;AACrD,UAAA,MAAM,QAAQ,CAAC,SAAS;AAC5B,WAAK,UAAU,CAAC;AAAA,IAAA,CACjB;AAAA,EACH;AAEO,SAAA;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ,CAAC;;"}
|
||||
{"version":3,"file":"cart.js","sources":["store/cart.ts"],"sourcesContent":["import { defineStore } from 'pinia'\r\nimport { ref, computed } from 'vue'\r\nimport type { CartItem } from '../types'\r\nimport { getCartList, addToCart as apiAddToCart, deleteCartItem as apiDeleteCartItem, updateCartItem as apiUpdateCartItem } from '../api/cart'\r\n\r\nexport const useCartStore = defineStore('cart', () => {\r\n const items = ref<CartItem[]>([])\r\n\r\n const checkedItems = computed(() => items.value.filter((item) => item.checked))\r\n\r\n const totalAmount = computed(() =>\r\n checkedItems.value.reduce((sum, item) => sum + item.specData.totalPrice * item.quantity, 0),\r\n )\r\n\r\n async function fetchCart() {\r\n try {\r\n const list = await getCartList()\r\n items.value = list.map((item) => ({ ...item, checked: true }))\r\n } catch {\r\n // 未登录或网络异常,保留本地数据\r\n }\r\n }\r\n\r\n function addToCart(item: CartItem) {\r\n items.value.push(item)\r\n apiAddToCart({\r\n productId: item.productId,\r\n specDataId: item.specDataId,\r\n quantity: item.quantity,\r\n }).catch(() => { /* 静默处理 */ })\r\n }\r\n\r\n function removeFromCart(id: number) {\r\n const index = items.value.findIndex((item) => item.id === id)\r\n if (index !== -1) {\r\n items.value.splice(index, 1)\r\n apiDeleteCartItem(id).catch(() => { /* 静默处理 */ })\r\n }\r\n }\r\n\r\n function updateQuantity(id: number, quantity: number) {\r\n const item = items.value.find((item) => item.id === id)\r\n if (item) {\r\n item.quantity = quantity\r\n apiUpdateCartItem(id, { quantity }).catch(() => { /* 静默处理 */ })\r\n }\r\n }\r\n\r\n /** 切换勾选状态(纯本地操作) */\r\n function toggleCheck(id: number) {\r\n const item = items.value.find((item) => item.id === id)\r\n if (item) {\r\n item.checked = !item.checked\r\n }\r\n }\r\n\r\n /** 全选/取消全选(纯本地操作) */\r\n function toggleCheckAll() {\r\n const allChecked = items.value.every((item) => item.checked)\r\n items.value.forEach((item) => {\r\n item.checked = !allChecked\r\n })\r\n }\r\n\r\n return {\r\n items,\r\n checkedItems,\r\n totalAmount,\r\n fetchCart,\r\n addToCart,\r\n removeFromCart,\r\n updateQuantity,\r\n toggleCheck,\r\n toggleCheckAll,\r\n }\r\n})\r\n"],"names":["defineStore","ref","computed","getCartList","apiAddToCart","apiDeleteCartItem","item","apiUpdateCartItem"],"mappings":";;;AAKa,MAAA,eAAeA,cAAAA,YAAY,QAAQ,MAAM;AAC9C,QAAA,QAAQC,kBAAgB,CAAA,CAAE;AAE1B,QAAA,eAAeC,cAAAA,SAAS,MAAM,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,OAAO,CAAC;AAE9E,QAAM,cAAcA,cAAA;AAAA,IAAS,MAC3B,aAAa,MAAM,OAAO,CAAC,KAAK,SAAS,MAAM,KAAK,SAAS,aAAa,KAAK,UAAU,CAAC;AAAA,EAAA;AAG5F,iBAAe,YAAY;AACrB,QAAA;AACI,YAAA,OAAO,MAAMC,SAAAA;AACb,YAAA,QAAQ,KAAK,IAAI,CAAC,UAAU,EAAE,GAAG,MAAM,SAAS,KAAA,EAAO;AAAA,IAAA,QACvD;AAAA,IAER;AAAA,EACF;AAEA,WAAS,UAAU,MAAgB;AAC3B,UAAA,MAAM,KAAK,IAAI;AACRC,uBAAA;AAAA,MACX,WAAW,KAAK;AAAA,MAChB,YAAY,KAAK;AAAA,MACjB,UAAU,KAAK;AAAA,IAAA,CAChB,EAAE,MAAM,MAAM;AAAA,IAAA,CAAc;AAAA,EAC/B;AAEA,WAAS,eAAe,IAAY;AAC5B,UAAA,QAAQ,MAAM,MAAM,UAAU,CAAC,SAAS,KAAK,OAAO,EAAE;AAC5D,QAAI,UAAU,IAAI;AACV,YAAA,MAAM,OAAO,OAAO,CAAC;AACTC,8BAAA,EAAE,EAAE,MAAM,MAAM;AAAA,MAAA,CAAc;AAAA,IAClD;AAAA,EACF;AAES,WAAA,eAAe,IAAY,UAAkB;AAC9C,UAAA,OAAO,MAAM,MAAM,KAAK,CAACC,UAASA,MAAK,OAAO,EAAE;AACtD,QAAI,MAAM;AACR,WAAK,WAAW;AAChBC,eAAAA,eAAkB,IAAI,EAAE,SAAA,CAAU,EAAE,MAAM,MAAM;AAAA,MAAA,CAAc;AAAA,IAChE;AAAA,EACF;AAGA,WAAS,YAAY,IAAY;AACzB,UAAA,OAAO,MAAM,MAAM,KAAK,CAACD,UAASA,MAAK,OAAO,EAAE;AACtD,QAAI,MAAM;AACH,WAAA,UAAU,CAAC,KAAK;AAAA,IACvB;AAAA,EACF;AAGA,WAAS,iBAAiB;AACxB,UAAM,aAAa,MAAM,MAAM,MAAM,CAAC,SAAS,KAAK,OAAO;AACrD,UAAA,MAAM,QAAQ,CAAC,SAAS;AAC5B,WAAK,UAAU,CAAC;AAAA,IAAA,CACjB;AAAA,EACH;AAEO,SAAA;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ,CAAC;;"}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,4 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
||||
const utils_request = require("../utils/request.js");
|
||||
const getCartList = () => utils_request.get("/api/cart");
|
||||
const addToCart = (data) => utils_request.post("/api/cart", data);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
"use strict";
|
||||
const _imports_0$1 = "/static/tab/me_s.png";
|
||||
const _imports_0$4 = "/static/ic_search.png";
|
||||
const _imports_0$3 = "/static/ic_back.png";
|
||||
const _imports_2$1 = "/static/ic_customer.png";
|
||||
const _imports_2 = "/static/tab/car.png";
|
||||
const _imports_0$2 = "/static/tab/me_s.png";
|
||||
const _imports_1 = "/static/ic_address.png";
|
||||
const _imports_2 = "/static/ic_customer.png";
|
||||
const _imports_3 = "/static/ic_about.png";
|
||||
const _imports_4 = "/static/ic_agreement1.png";
|
||||
const _imports_5 = "/static/ic_agreement2.png";
|
||||
const _imports_0 = "/static/logo.png";
|
||||
exports._imports_0 = _imports_0$1;
|
||||
exports._imports_0$1 = _imports_0;
|
||||
const _imports_0$1 = "/static/logo.png";
|
||||
const _imports_0 = "/static/ic_notice.png";
|
||||
exports._imports_0 = _imports_0$4;
|
||||
exports._imports_0$1 = _imports_0$3;
|
||||
exports._imports_0$2 = _imports_0$2;
|
||||
exports._imports_0$3 = _imports_0$1;
|
||||
exports._imports_0$4 = _imports_0;
|
||||
exports._imports_1 = _imports_1;
|
||||
exports._imports_2 = _imports_2;
|
||||
exports._imports_2 = _imports_2$1;
|
||||
exports._imports_2$1 = _imports_2;
|
||||
exports._imports_3 = _imports_3;
|
||||
exports._imports_4 = _imports_4;
|
||||
exports._imports_5 = _imports_5;
|
||||
|
|
|
|||
|
|
@ -7041,7 +7041,7 @@ function isConsoleWritable() {
|
|||
function initRuntimeSocketService() {
|
||||
const hosts = "172.31.144.1,192.168.21.7,192.168.195.32,127.0.0.1";
|
||||
const port = "8090";
|
||||
const id = "mp-weixin_ucp1tM";
|
||||
const id = "mp-weixin_9p8dl1";
|
||||
const lazy = typeof swan !== "undefined";
|
||||
let restoreError = lazy ? () => {
|
||||
} : initOnError();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use strict";
|
||||
const common_vendor = require("../common/vendor.js");
|
||||
const utils_request = require("../utils/request.js");
|
||||
const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
||||
__name: "BannerSwiper",
|
||||
props: {
|
||||
|
|
@ -7,15 +8,22 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
video: {}
|
||||
},
|
||||
setup(__props) {
|
||||
function fullUrl(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
if (path.startsWith("http"))
|
||||
return path;
|
||||
return utils_request.BASE_URL + path;
|
||||
}
|
||||
return (_ctx, _cache) => {
|
||||
return common_vendor.e({
|
||||
a: _ctx.video
|
||||
}, _ctx.video ? {
|
||||
b: _ctx.video
|
||||
b: fullUrl(_ctx.video)
|
||||
} : {}, {
|
||||
c: common_vendor.f(_ctx.images, (img, idx, i0) => {
|
||||
return {
|
||||
a: img,
|
||||
a: fullUrl(img),
|
||||
b: idx
|
||||
};
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
|
||||
.banner-swiper.data-v-0577c1c5 {
|
||||
width: 100%;
|
||||
height: 600rpx;
|
||||
.banner-swiper.data-v-0577c1c5 { width: 100%; height: 600rpx;
|
||||
}
|
||||
.banner-swiper__image.data-v-0577c1c5 {
|
||||
width: 100%;
|
||||
height: 600rpx;
|
||||
.banner-swiper__image.data-v-0577c1c5 { width: 100%; height: 600rpx;
|
||||
}
|
||||
.banner-swiper__video.data-v-0577c1c5 {
|
||||
width: 100%;
|
||||
height: 600rpx;
|
||||
.banner-swiper__video.data-v-0577c1c5 { width: 100%; height: 600rpx;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
return common_vendor.e({
|
||||
a: _ctx.mode === "qrcode"
|
||||
}, _ctx.mode === "qrcode" ? {
|
||||
b: common_assets._imports_0$1,
|
||||
b: common_assets._imports_0$3,
|
||||
c: common_vendor.o(($event) => _ctx.$emit("close")),
|
||||
d: common_vendor.o(() => {
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use strict";
|
||||
const common_vendor = require("../common/vendor.js");
|
||||
const utils_request = require("../utils/request.js");
|
||||
const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
||||
__name: "ProductCard",
|
||||
props: {
|
||||
|
|
@ -7,12 +8,21 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
},
|
||||
setup(__props) {
|
||||
const props = __props;
|
||||
function imgSrc() {
|
||||
var _a;
|
||||
const url = props.product.thumb || ((_a = props.product.bannerImages) == null ? void 0 : _a[0]);
|
||||
if (!url)
|
||||
return "/static/logo.png";
|
||||
if (url.startsWith("http"))
|
||||
return url;
|
||||
return utils_request.BASE_URL + url;
|
||||
}
|
||||
function goDetail() {
|
||||
common_vendor.index.navigateTo({ url: `/pages/product/detail?id=${props.product.id}` });
|
||||
}
|
||||
return (_ctx, _cache) => {
|
||||
return {
|
||||
a: _ctx.product.bannerImages[0] || "/static/logo.png",
|
||||
a: imgSrc(),
|
||||
b: common_vendor.t(_ctx.product.name),
|
||||
c: common_vendor.t(_ctx.product.styleNo),
|
||||
d: common_vendor.t(_ctx.product.basePrice),
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<view class="product-card data-v-fe52aa40" bindtap="{{f}}"><image class="product-card__image data-v-fe52aa40" src="{{a}}" mode="aspectFill"/><view class="product-card__info data-v-fe52aa40"><text class="product-card__name data-v-fe52aa40">{{b}}</text><text class="product-card__style data-v-fe52aa40">款号:{{c}}</text><view class="product-card__bottom data-v-fe52aa40"><text class="product-card__price data-v-fe52aa40">¥{{d}}</text><text class="product-card__stock data-v-fe52aa40">库存 {{e}}</text></view></view></view>
|
||||
<view class="product-card data-v-fe52aa40" bindtap="{{f}}"><image class="product-card__image data-v-fe52aa40" src="{{a}}" mode="aspectFill"/><view class="product-card__info data-v-fe52aa40"><text class="product-card__name data-v-fe52aa40">{{b}}({{c}})</text><view class="product-card__bottom data-v-fe52aa40"><view class="product-card__price-tag data-v-fe52aa40"><text class="product-card__price data-v-fe52aa40">¥{{d}}</text></view><text class="product-card__stock data-v-fe52aa40">库存{{e}}</text></view></view></view>
|
||||
|
|
@ -1,45 +1,45 @@
|
|||
|
||||
.product-card.data-v-fe52aa40 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.product-card__image.data-v-fe52aa40 {
|
||||
width: 100%;
|
||||
height: 340rpx;
|
||||
width: 100%;
|
||||
height: 340rpx;
|
||||
}
|
||||
.product-card__info.data-v-fe52aa40 {
|
||||
padding: 16rpx;
|
||||
padding: 16rpx 20rpx 20rpx;
|
||||
}
|
||||
.product-card__name.data-v-fe52aa40 {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
.product-card__style.data-v-fe52aa40 {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.product-card__bottom.data-v-fe52aa40 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.product-card__price-tag.data-v-fe52aa40 {
|
||||
background: linear-gradient(135deg, #f5a0b8, #FF6D9B);
|
||||
border-radius: 8rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
}
|
||||
.product-card__price.data-v-fe52aa40 {
|
||||
font-size: 32rpx;
|
||||
color: #e4393c;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.product-card__stock.data-v-fe52aa40 {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
"use strict";
|
||||
const common_assets = require("../common/assets.js");
|
||||
const common_vendor = require("../common/vendor.js");
|
||||
const _sfc_main = {};
|
||||
function _sfc_render(_ctx, _cache) {
|
||||
return {};
|
||||
return {
|
||||
a: common_assets._imports_0$4
|
||||
};
|
||||
}
|
||||
const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-807004e7"]]);
|
||||
wx.createComponent(Component);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<view class="shipping-notice data-v-807004e7"><view class="shipping-notice__title data-v-807004e7">发货公告</view><view class="shipping-notice__item data-v-807004e7">交易方式:线下支付,微信沟通确认</view><view class="shipping-notice__item data-v-807004e7">客服微信:请点击「联系客服」获取</view><view class="shipping-notice__item data-v-807004e7">公司地址:请联系客服获取详细地址</view></view>
|
||||
<view class="shipping-notice data-v-807004e7"><view class="shipping-notice__header data-v-807004e7"><image class="shipping-notice__icon data-v-807004e7" src="{{a}}" mode="aspectFit"/><text class="shipping-notice__title data-v-807004e7">发货公告:</text></view><view class="shipping-notice__body data-v-807004e7"><text class="shipping-notice__item data-v-807004e7">叶生珠宝-空托之城空托都是当天金工石结算</text><text class="shipping-notice__item data-v-807004e7">客服微信:15920028399</text><text class="shipping-notice__item data-v-807004e7">交易方式:加微信门店交易,支付宝,微信,银行卡转账</text><text class="shipping-notice__item data-v-807004e7">公司地址:深圳市罗湖区水贝二路贝丽花园21栋108叶生珠宝</text></view></view>
|
||||
|
|
@ -1,18 +1,32 @@
|
|||
|
||||
.shipping-notice.data-v-807004e7 {
|
||||
background: #fffbe6;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
margin: 20rpx 24rpx;
|
||||
background: #fce4ec;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
margin: 20rpx 24rpx 0;
|
||||
}
|
||||
.shipping-notice__header.data-v-807004e7 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.shipping-notice__icon.data-v-807004e7 {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.shipping-notice__title.data-v-807004e7 {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #fa8c16;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 600;
|
||||
color: #e91e63;
|
||||
}
|
||||
.shipping-notice__body.data-v-807004e7 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.shipping-notice__item.data-v-807004e7 {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 40rpx;
|
||||
font-size: 26rpx;
|
||||
color: #e91e63;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,58 @@
|
|||
"use strict";
|
||||
const common_vendor = require("../common/vendor.js");
|
||||
const api_product = require("../api/product.js");
|
||||
const store_cart = require("../store/cart.js");
|
||||
const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
||||
__name: "SpecPanel",
|
||||
props: {
|
||||
productId: {}
|
||||
},
|
||||
emits: ["close"],
|
||||
setup(__props) {
|
||||
setup(__props, { emit: __emit }) {
|
||||
const props = __props;
|
||||
const emit = __emit;
|
||||
const config = common_vendor.ref(null);
|
||||
const loadingConfig = common_vendor.ref(false);
|
||||
const loadingData = common_vendor.ref(false);
|
||||
const specDataList = common_vendor.ref([]);
|
||||
const selected = common_vendor.reactive({
|
||||
fineness: "",
|
||||
mainStone: "",
|
||||
ringSize: ""
|
||||
});
|
||||
const canQuery = common_vendor.computed(
|
||||
() => selected.fineness && selected.mainStone && selected.ringSize
|
||||
);
|
||||
const selectedSpecs = common_vendor.ref(/* @__PURE__ */ new Map());
|
||||
const cartStore = store_cart.useCartStore();
|
||||
const selected = common_vendor.reactive({ fineness: "", mainStone: "", ringSize: "" });
|
||||
function selectOption(key, item) {
|
||||
selected[key] = selected[key] === item ? "" : item;
|
||||
if (selected.fineness || selected.mainStone || selected.ringSize) {
|
||||
querySpecData();
|
||||
} else {
|
||||
specDataList.value = [];
|
||||
}
|
||||
}
|
||||
function toggleSelect(spec) {
|
||||
if (selectedSpecs.value.has(spec.id)) {
|
||||
selectedSpecs.value.delete(spec.id);
|
||||
} else {
|
||||
selectedSpecs.value.set(spec.id, spec);
|
||||
}
|
||||
selectedSpecs.value = new Map(selectedSpecs.value);
|
||||
}
|
||||
function handleAddToCart() {
|
||||
if (selectedSpecs.value.size === 0)
|
||||
return;
|
||||
selectedSpecs.value.forEach((spec) => {
|
||||
cartStore.addToCart({
|
||||
id: Date.now() + spec.id,
|
||||
userId: 0,
|
||||
productId: props.productId,
|
||||
specDataId: spec.id,
|
||||
quantity: 1,
|
||||
product: {},
|
||||
specData: spec,
|
||||
checked: true
|
||||
});
|
||||
});
|
||||
common_vendor.index.showToast({ title: `已加入${selectedSpecs.value.size}件`, icon: "success" });
|
||||
selectedSpecs.value = /* @__PURE__ */ new Map();
|
||||
emit("close");
|
||||
}
|
||||
common_vendor.onMounted(async () => {
|
||||
loadingConfig.value = true;
|
||||
try {
|
||||
|
|
@ -31,14 +63,12 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
}
|
||||
});
|
||||
async function querySpecData() {
|
||||
if (!canQuery.value)
|
||||
return;
|
||||
loadingData.value = true;
|
||||
try {
|
||||
specDataList.value = await api_product.getSpecDataList(props.productId, {
|
||||
fineness: selected.fineness,
|
||||
mainStone: selected.mainStone,
|
||||
ringSize: selected.ringSize
|
||||
fineness: selected.fineness || void 0,
|
||||
mainStone: selected.mainStone || void 0,
|
||||
ringSize: selected.ringSize || void 0
|
||||
});
|
||||
} catch {
|
||||
} finally {
|
||||
|
|
@ -47,63 +77,88 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
}
|
||||
return (_ctx, _cache) => {
|
||||
return common_vendor.e({
|
||||
a: common_vendor.o(($event) => _ctx.$emit("close")),
|
||||
b: loadingConfig.value
|
||||
a: loadingConfig.value
|
||||
}, loadingConfig.value ? {} : config.value ? common_vendor.e({
|
||||
d: config.value.fineness.length
|
||||
c: config.value.fineness.length
|
||||
}, config.value.fineness.length ? {
|
||||
e: common_vendor.f(config.value.fineness, (item, k0, i0) => {
|
||||
d: common_vendor.f(config.value.fineness, (item, k0, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(item),
|
||||
b: item,
|
||||
c: selected.fineness === item ? 1 : "",
|
||||
d: common_vendor.o(($event) => selected.fineness = item, item)
|
||||
d: common_vendor.o(($event) => selectOption("fineness", item), item)
|
||||
};
|
||||
})
|
||||
} : {}, {
|
||||
f: config.value.mainStone.length
|
||||
e: config.value.mainStone.length
|
||||
}, config.value.mainStone.length ? {
|
||||
g: common_vendor.f(config.value.mainStone, (item, k0, i0) => {
|
||||
f: common_vendor.f(config.value.mainStone, (item, k0, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(item),
|
||||
b: item,
|
||||
c: selected.mainStone === item ? 1 : "",
|
||||
d: common_vendor.o(($event) => selected.mainStone = item, item)
|
||||
d: common_vendor.o(($event) => selectOption("mainStone", item), item)
|
||||
};
|
||||
})
|
||||
} : {}, {
|
||||
h: config.value.ringSize.length
|
||||
g: config.value.ringSize.length
|
||||
}, config.value.ringSize.length ? {
|
||||
i: common_vendor.f(config.value.ringSize, (item, k0, i0) => {
|
||||
h: common_vendor.f(config.value.ringSize, (item, k0, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(item),
|
||||
b: item,
|
||||
c: selected.ringSize === item ? 1 : "",
|
||||
d: common_vendor.o(($event) => selected.ringSize = item, item)
|
||||
d: common_vendor.o(($event) => selectOption("ringSize", item), item)
|
||||
};
|
||||
})
|
||||
} : {}, {
|
||||
j: !canQuery.value ? 1 : "",
|
||||
k: common_vendor.o(querySpecData),
|
||||
l: loadingData.value
|
||||
} : {}) : {}, {
|
||||
b: config.value,
|
||||
i: loadingData.value
|
||||
}, loadingData.value ? {} : specDataList.value.length ? {
|
||||
n: common_vendor.f(specDataList.value, (spec, k0, i0) => {
|
||||
k: common_vendor.f(specDataList.value, (spec, k0, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(spec.modelName),
|
||||
b: common_vendor.t(spec.goldTotalWeight),
|
||||
c: common_vendor.t(spec.goldValue),
|
||||
d: common_vendor.t(spec.mainStoneAmount),
|
||||
e: common_vendor.t(spec.sideStoneAmount),
|
||||
f: common_vendor.t(spec.totalLaborCost),
|
||||
g: common_vendor.t(spec.totalPrice),
|
||||
h: spec.id
|
||||
b: common_vendor.t(spec.fineness),
|
||||
c: common_vendor.t(spec.goldPrice),
|
||||
d: common_vendor.t(spec.modelName),
|
||||
e: common_vendor.t(spec.goldTotalWeight),
|
||||
f: common_vendor.t(spec.goldNetWeight),
|
||||
g: common_vendor.t(spec.goldLoss),
|
||||
h: common_vendor.t(spec.goldValue),
|
||||
i: common_vendor.t(spec.mainStoneCount),
|
||||
j: common_vendor.t(spec.mainStoneWeight),
|
||||
k: common_vendor.t(spec.mainStoneUnitPrice),
|
||||
l: common_vendor.t(spec.mainStoneAmount),
|
||||
m: common_vendor.t(spec.accessoryAmount),
|
||||
n: common_vendor.t(spec.totalLaborCost),
|
||||
o: common_vendor.t(spec.fineness),
|
||||
p: common_vendor.t(spec.loss),
|
||||
q: common_vendor.t(spec.mainStone),
|
||||
r: common_vendor.t(spec.ringSize),
|
||||
s: common_vendor.t(spec.goldPrice),
|
||||
t: common_vendor.t(spec.sideStoneCount),
|
||||
v: common_vendor.t(spec.sideStoneWeight),
|
||||
w: common_vendor.t(spec.sideStoneUnitPrice),
|
||||
x: common_vendor.t(spec.sideStoneAmount),
|
||||
y: common_vendor.t(spec.processingFee),
|
||||
z: common_vendor.t(spec.settingFee),
|
||||
A: common_vendor.t(spec.totalPrice),
|
||||
B: spec.id,
|
||||
C: selectedSpecs.value.has(spec.id) ? 1 : "",
|
||||
D: common_vendor.o(($event) => toggleSelect(spec), spec.id)
|
||||
};
|
||||
})
|
||||
} : {}, {
|
||||
m: specDataList.value.length
|
||||
}) : {}, {
|
||||
c: config.value,
|
||||
o: common_vendor.o(($event) => _ctx.$emit("close"))
|
||||
j: specDataList.value.length,
|
||||
l: selectedSpecs.value.size
|
||||
}, selectedSpecs.value.size ? {
|
||||
m: common_vendor.t(selectedSpecs.value.size)
|
||||
} : {}, {
|
||||
n: selectedSpecs.value.size === 0 ? 1 : "",
|
||||
o: common_vendor.o(handleAddToCart),
|
||||
p: common_vendor.o(() => {
|
||||
}),
|
||||
q: common_vendor.o(($event) => _ctx.$emit("close"))
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,114 +1,86 @@
|
|||
|
||||
.spec-panel-mask.data-v-c30573e9 {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5); z-index: 999;
|
||||
display: flex; align-items: flex-end;
|
||||
}
|
||||
.spec-panel.data-v-c30573e9 {
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
background: #fff; width: 100%; height: 85vh;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding: 32rpx 24rpx;
|
||||
overflow-y: auto;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.spec-panel__header.data-v-c30573e9 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.spec-panel__title.data-v-c30573e9 {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.spec-panel__close.data-v-c30573e9 {
|
||||
font-size: 36rpx;
|
||||
color: #999;
|
||||
padding: 8rpx;
|
||||
.spec-panel__scroll.data-v-c30573e9 {
|
||||
flex: 1; padding: 32rpx 28rpx 0; overflow: hidden;
|
||||
}
|
||||
.spec-panel__loading.data-v-c30573e9 {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx;
|
||||
}
|
||||
.spec-group.data-v-c30573e9 {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
/* 规格分组 */
|
||||
.spec-group.data-v-c30573e9 { margin-bottom: 32rpx;
|
||||
}
|
||||
.spec-group__label.data-v-c30573e9 {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
font-size: 30rpx; color: #333; font-weight: 600;
|
||||
margin-bottom: 20rpx; display: block; letter-spacing: 8rpx;
|
||||
}
|
||||
.spec-group__options.data-v-c30573e9 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
.spec-group__options.data-v-c30573e9 { display: flex; flex-wrap: wrap; gap: 16rpx;
|
||||
}
|
||||
.spec-option.data-v-c30573e9 {
|
||||
padding: 12rpx 28rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid transparent;
|
||||
padding: 14rpx 32rpx; font-size: 26rpx; color: #333;
|
||||
background: #f5f5f5; border-radius: 8rpx; border: 2rpx solid #f5f5f5;
|
||||
}
|
||||
.spec-option--active.data-v-c30573e9 {
|
||||
color: #e4393c;
|
||||
background: #fff1f0;
|
||||
border-color: #e4393c;
|
||||
color: #e91e63; background: #fce4ec; border-color: #e91e63;
|
||||
}
|
||||
.spec-panel__action.data-v-c30573e9 {
|
||||
margin: 24rpx 0;
|
||||
|
||||
/* 规格数据卡片 */
|
||||
.spec-data-list.data-v-c30573e9 { padding-bottom: 20rpx;
|
||||
}
|
||||
.spec-panel__btn.data-v-c30573e9 {
|
||||
background: #e4393c;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 44rpx;
|
||||
font-size: 28rpx;
|
||||
.spec-data-card.data-v-c30573e9 {
|
||||
border: 2rpx solid #f0e0e0; border-radius: 16rpx;
|
||||
padding: 24rpx; margin-bottom: 20rpx; background: #fff;
|
||||
}
|
||||
.spec-panel__btn--disabled.data-v-c30573e9 {
|
||||
background: #ccc;
|
||||
.spec-data-card--selected.data-v-c30573e9 {
|
||||
border-color: #e91e63; background: #fff5f7;
|
||||
}
|
||||
.spec-data-list.data-v-c30573e9 {
|
||||
margin-top: 16rpx;
|
||||
.spec-card__header.data-v-c30573e9 {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 16rpx; padding-bottom: 16rpx; border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.spec-data-item.data-v-c30573e9 {
|
||||
background: #fafafa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
.spec-card__title.data-v-c30573e9 { font-size: 26rpx; color: #333; font-weight: 600;
|
||||
}
|
||||
.spec-data-row.data-v-c30573e9 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6rpx 0;
|
||||
.spec-card__gold-price.data-v-c30573e9 { font-size: 24rpx; color: #666;
|
||||
}
|
||||
.spec-data-row--total.data-v-c30573e9 {
|
||||
border-top: 1rpx solid #eee;
|
||||
margin-top: 8rpx;
|
||||
padding-top: 12rpx;
|
||||
.spec-card__body.data-v-c30573e9 { display: flex; gap: 12rpx;
|
||||
}
|
||||
.spec-data-label.data-v-c30573e9 {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
.spec-card__col.data-v-c30573e9 { flex: 1;
|
||||
}
|
||||
.spec-data-value.data-v-c30573e9 {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
.spec-card__row.data-v-c30573e9 { display: flex; justify-content: space-between; padding: 5rpx 0;
|
||||
}
|
||||
.spec-data-value--price.data-v-c30573e9 {
|
||||
color: #e4393c;
|
||||
font-weight: bold;
|
||||
.spec-card__label.data-v-c30573e9 { font-size: 23rpx; color: #999; flex-shrink: 0;
|
||||
}
|
||||
.spec-card__value.data-v-c30573e9 { font-size: 23rpx; color: #333; text-align: right;
|
||||
}
|
||||
.spec-card__footer.data-v-c30573e9 {
|
||||
display: flex; justify-content: flex-end; align-items: center;
|
||||
margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid #f0f0f0; gap: 12rpx;
|
||||
}
|
||||
.spec-card__total-label.data-v-c30573e9 { font-size: 26rpx; color: #e91e63;
|
||||
}
|
||||
.spec-card__total-price.data-v-c30573e9 { font-size: 32rpx; color: #e91e63; font-weight: bold;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.spec-panel__bottom.data-v-c30573e9 {
|
||||
padding: 20rpx 28rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #fff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
.spec-panel__cart-btn.data-v-c30573e9 {
|
||||
background: linear-gradient(to right, #FFB6C8, #FF6D9B);
|
||||
color: #fff; text-align: center; padding: 24rpx 0;
|
||||
border-radius: 44rpx; font-size: 30rpx; font-weight: 500;
|
||||
}
|
||||
.spec-panel__cart-btn--disabled.data-v-c30573e9 {
|
||||
background: linear-gradient(to right, #ddd, #ccc);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_assets = require("../../common/assets.js");
|
||||
const api_product = require("../../api/product.js");
|
||||
const utils_request = require("../../utils/request.js");
|
||||
if (!Math) {
|
||||
ProductCard();
|
||||
(ProductCard + CustomerServiceBtn)();
|
||||
}
|
||||
const ProductCard = () => "../../components/ProductCard.js";
|
||||
const CustomerServiceBtn = () => "../../components/CustomerServiceBtn.js";
|
||||
const pageSize = 20;
|
||||
const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
||||
__name: "index",
|
||||
|
|
@ -13,11 +16,29 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
const categories = common_vendor.ref([]);
|
||||
const activeCategoryId = common_vendor.ref(void 0);
|
||||
const loading = common_vendor.ref(false);
|
||||
const showQrcode = common_vendor.ref(false);
|
||||
const page = common_vendor.ref(1);
|
||||
const keyword = common_vendor.ref("");
|
||||
const statusBarHeight = common_vendor.ref(20);
|
||||
const navBarHeight = common_vendor.ref(44);
|
||||
try {
|
||||
const sysInfo = common_vendor.index.getSystemInfoSync();
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 20;
|
||||
const menuBtn = common_vendor.index.getMenuButtonBoundingClientRect();
|
||||
navBarHeight.value = (menuBtn.top - (sysInfo.statusBarHeight || 20)) * 2 + menuBtn.height;
|
||||
} catch {
|
||||
}
|
||||
function fullUrl(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
if (path.startsWith("http"))
|
||||
return path;
|
||||
return utils_request.BASE_URL + path;
|
||||
}
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const data = await api_product.getCategories();
|
||||
categories.value = [{ id: 0, name: "全部", sort: 0 }, ...data];
|
||||
categories.value = data;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
|
@ -29,9 +50,10 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
loading.value = true;
|
||||
try {
|
||||
const params = { page: page.value, pageSize };
|
||||
if (activeCategoryId.value && activeCategoryId.value !== 0) {
|
||||
if (activeCategoryId.value)
|
||||
params.categoryId = activeCategoryId.value;
|
||||
}
|
||||
if (keyword.value)
|
||||
params.keyword = keyword.value;
|
||||
const data = await api_product.getProducts(params);
|
||||
if (reset) {
|
||||
products.value = data.list;
|
||||
|
|
@ -44,24 +66,40 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
}
|
||||
}
|
||||
function selectCategory(id) {
|
||||
activeCategoryId.value = id;
|
||||
activeCategoryId.value = activeCategoryId.value === id ? void 0 : id;
|
||||
loadProducts(true);
|
||||
}
|
||||
function goSearch() {
|
||||
}
|
||||
function goCalculator() {
|
||||
common_vendor.index.navigateTo({ url: "/pages/calculator/index" });
|
||||
}
|
||||
common_vendor.onMounted(() => {
|
||||
loadCategories();
|
||||
loadProducts(true);
|
||||
});
|
||||
return (_ctx, _cache) => {
|
||||
return common_vendor.e({
|
||||
a: common_vendor.f(categories.value, (cat, k0, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(cat.name),
|
||||
b: cat.id,
|
||||
c: activeCategoryId.value === cat.id ? 1 : "",
|
||||
d: common_vendor.o(($event) => selectCategory(cat.id), cat.id)
|
||||
};
|
||||
a: navBarHeight.value + "px",
|
||||
b: statusBarHeight.value + "px",
|
||||
c: common_assets._imports_0,
|
||||
d: common_vendor.o(goSearch),
|
||||
e: common_vendor.o(goSearch),
|
||||
f: common_vendor.f(categories.value, (cat, k0, i0) => {
|
||||
return common_vendor.e({
|
||||
a: cat.icon
|
||||
}, cat.icon ? {
|
||||
b: fullUrl(cat.icon)
|
||||
} : {}, {
|
||||
c: common_vendor.t(cat.name),
|
||||
d: cat.id,
|
||||
e: activeCategoryId.value === cat.id ? 1 : "",
|
||||
f: common_vendor.o(($event) => selectCategory(cat.id), cat.id)
|
||||
});
|
||||
}),
|
||||
b: common_vendor.f(products.value, (product, k0, i0) => {
|
||||
g: common_vendor.o(goCalculator),
|
||||
h: common_vendor.o(($event) => showQrcode.value = true),
|
||||
i: common_vendor.f(products.value, (product, k0, i0) => {
|
||||
return {
|
||||
a: "1cf27b2a-0-" + i0,
|
||||
b: common_vendor.p({
|
||||
|
|
@ -70,10 +108,17 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
c: product.id
|
||||
};
|
||||
}),
|
||||
c: !loading.value && products.value.length === 0
|
||||
j: !loading.value && products.value.length === 0
|
||||
}, !loading.value && products.value.length === 0 ? {} : {}, {
|
||||
d: loading.value
|
||||
}, loading.value ? {} : {});
|
||||
k: loading.value
|
||||
}, loading.value ? {} : {}, {
|
||||
l: showQrcode.value
|
||||
}, showQrcode.value ? {
|
||||
m: common_vendor.o(($event) => showQrcode.value = false),
|
||||
n: common_vendor.p({
|
||||
mode: "qrcode"
|
||||
})
|
||||
} : {});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"navigationBarTitleText": "珠宝商城",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"product-card": "../../components/ProductCard"
|
||||
"product-card": "../../components/ProductCard",
|
||||
"customer-service-btn": "../../components/CustomerServiceBtn"
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
<view class="home-page data-v-1cf27b2a"><scroll-view class="category-bar data-v-1cf27b2a" scroll-x><view wx:for="{{a}}" wx:for-item="cat" wx:key="b" class="{{['category-item', 'data-v-1cf27b2a', cat.c && 'category-item--active']}}" bindtap="{{cat.d}}">{{cat.a}}</view></scroll-view><view class="product-grid data-v-1cf27b2a"><view wx:for="{{b}}" wx:for-item="product" wx:key="c" class="product-grid__item data-v-1cf27b2a"><product-card wx:if="{{product.b}}" class="data-v-1cf27b2a" u-i="{{product.a}}" bind:__l="__l" u-p="{{product.b}}"/></view></view><view wx:if="{{c}}" class="empty-tip data-v-1cf27b2a"><text class="data-v-1cf27b2a">暂无商品</text></view><view wx:if="{{d}}" class="loading-tip data-v-1cf27b2a"><text class="data-v-1cf27b2a">加载中...</text></view></view>
|
||||
<view class="home-page data-v-1cf27b2a"><view class="custom-navbar data-v-1cf27b2a" style="{{'padding-top:' + b}}"><view class="custom-navbar__content data-v-1cf27b2a" style="{{'height:' + a}}"><text class="custom-navbar__title data-v-1cf27b2a">凯缘钻之城</text></view></view><view class="search-bar data-v-1cf27b2a"><view class="search-bar__input data-v-1cf27b2a" bindtap="{{d}}"><image class="search-bar__icon data-v-1cf27b2a" src="{{c}}" mode="aspectFit"/><text class="search-bar__placeholder data-v-1cf27b2a">请输入产品名称、款号、条码号或款式</text></view><text class="search-bar__btn data-v-1cf27b2a" bindtap="{{e}}">搜索</text></view><scroll-view class="category-section data-v-1cf27b2a" scroll-x><view class="category-section__inner data-v-1cf27b2a"><view wx:for="{{f}}" wx:for-item="cat" wx:key="d" class="{{['category-icon', 'data-v-1cf27b2a', cat.e && 'category-icon--active']}}" bindtap="{{cat.f}}"><view class="category-icon__circle data-v-1cf27b2a"><image wx:if="{{cat.a}}" class="category-icon__img data-v-1cf27b2a" src="{{cat.b}}" mode="aspectFill"/><text wx:else class="category-icon__emoji data-v-1cf27b2a">💎</text></view><text class="category-icon__label data-v-1cf27b2a">{{cat.c}}</text></view></view></scroll-view><view class="quick-actions data-v-1cf27b2a"><view class="quick-action quick-action--calc data-v-1cf27b2a" bindtap="{{g}}"><text class="quick-action__icon data-v-1cf27b2a">💎</text><text class="quick-action__text data-v-1cf27b2a">钻戒计算器</text></view><view class="quick-action quick-action--service data-v-1cf27b2a" bindtap="{{h}}"><text class="quick-action__icon data-v-1cf27b2a">👩💼</text><text class="quick-action__text data-v-1cf27b2a">客服找款</text></view></view><view class="product-grid data-v-1cf27b2a"><view wx:for="{{i}}" wx:for-item="product" wx:key="c" class="product-grid__item data-v-1cf27b2a"><product-card wx:if="{{product.b}}" class="data-v-1cf27b2a" u-i="{{product.a}}" bind:__l="__l" u-p="{{product.b}}"/></view></view><view wx:if="{{j}}" class="empty-tip data-v-1cf27b2a"><text class="data-v-1cf27b2a">暂无商品</text></view><view wx:if="{{k}}" class="loading-tip data-v-1cf27b2a"><text class="data-v-1cf27b2a">加载中...</text></view><customer-service-btn wx:if="{{l}}" class="data-v-1cf27b2a" bindclose="{{m}}" u-i="1cf27b2a-1" bind:__l="__l" u-p="{{n}}"/></view>
|
||||
|
|
@ -1,39 +1,149 @@
|
|||
|
||||
.home-page.data-v-1cf27b2a {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.category-bar.data-v-1cf27b2a {
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
padding: 20rpx 16rpx;
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar.data-v-1cf27b2a {
|
||||
background: linear-gradient(to right, #FFCFDE, #FFA6C4);
|
||||
}
|
||||
.category-item.data-v-1cf27b2a {
|
||||
display: inline-block;
|
||||
padding: 12rpx 28rpx;
|
||||
margin-right: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
border-radius: 28rpx;
|
||||
.custom-navbar__content.data-v-1cf27b2a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.category-item--active.data-v-1cf27b2a {
|
||||
color: #fff;
|
||||
background: #e4393c;
|
||||
.custom-navbar__title.data-v-1cf27b2a {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar.data-v-1cf27b2a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
.search-bar__input.data-v-1cf27b2a {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 40rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
height: 72rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.search-bar__icon.data-v-1cf27b2a {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
.search-bar__placeholder.data-v-1cf27b2a {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.search-bar__btn.data-v-1cf27b2a {
|
||||
margin-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 分类图标 */
|
||||
.category-section.data-v-1cf27b2a {
|
||||
white-space: nowrap;
|
||||
padding: 32rpx 0 24rpx;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
.category-section__inner.data-v-1cf27b2a {
|
||||
display: inline-flex;
|
||||
padding: 0 24rpx;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.category-icon.data-v-1cf27b2a {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.category-icon__circle.data-v-1cf27b2a {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 30rpx;
|
||||
background: linear-gradient(135deg, #fce4ec, #f8bbd0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.category-icon--active .category-icon__circle.data-v-1cf27b2a {
|
||||
background: linear-gradient(135deg, #f48fb1, #e91e63);
|
||||
box-shadow: 0 4rpx 16rpx rgba(233, 30, 99, 0.3);
|
||||
}
|
||||
.category-icon__emoji.data-v-1cf27b2a {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
.category-icon__img.data-v-1cf27b2a {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
}
|
||||
.category-icon__label.data-v-1cf27b2a {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
.category-icon--active .category-icon__label.data-v-1cf27b2a {
|
||||
color: #e91e63;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 快捷入口 */
|
||||
.quick-actions.data-v-1cf27b2a {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 0 24rpx 24rpx;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
.quick-action.data-v-1cf27b2a {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx 0;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.quick-action--calc.data-v-1cf27b2a {
|
||||
background: linear-gradient(135deg, #FFA4C3, #FFD2E0);
|
||||
}
|
||||
.quick-action--service.data-v-1cf27b2a {
|
||||
background: linear-gradient(135deg, #e8f5e9, #fff);
|
||||
}
|
||||
.quick-action__icon.data-v-1cf27b2a {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
.quick-action__text.data-v-1cf27b2a {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 商品列表 */
|
||||
.product-grid.data-v-1cf27b2a {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16rpx;
|
||||
gap: 16rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 24rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.product-grid__item.data-v-1cf27b2a {
|
||||
width: calc(50% - 8rpx);
|
||||
width: calc(50% - 8rpx);
|
||||
}
|
||||
.empty-tip.data-v-1cf27b2a,
|
||||
.loading-tip.data-v-1cf27b2a {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
.loading-tip.data-v-1cf27b2a {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
}
|
||||
return (_ctx, _cache) => {
|
||||
return {
|
||||
a: common_assets._imports_0$1,
|
||||
a: common_assets._imports_0$3,
|
||||
b: loading.value,
|
||||
c: common_vendor.o(handleLogin),
|
||||
d: common_vendor.o(goBack)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
a: ((_a = common_vendor.unref(userStore).user) == null ? void 0 : _a.avatar) || "/static/logo.png",
|
||||
b: common_vendor.t(isLoggedIn.value ? ((_b = common_vendor.unref(userStore).user) == null ? void 0 : _b.nickname) || "微信用户" : "点击注册/登录"),
|
||||
c: common_vendor.o(handleUserCardClick),
|
||||
d: common_assets._imports_0,
|
||||
d: common_assets._imports_0$2,
|
||||
e: common_vendor.t(orderCount.value),
|
||||
f: common_vendor.o(($event) => navigateTo("/pages/order/list")),
|
||||
g: common_assets._imports_1,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_assets = require("../../common/assets.js");
|
||||
const api_product = require("../../api/product.js");
|
||||
const store_cart = require("../../store/cart.js");
|
||||
const utils_request = require("../../utils/request.js");
|
||||
if (!Math) {
|
||||
(BannerSwiper + ShippingNotice + SpecPanel + CustomerServiceBtn)();
|
||||
}
|
||||
|
|
@ -15,15 +16,35 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
const product = common_vendor.ref(null);
|
||||
const showSpecPanel = common_vendor.ref(false);
|
||||
const showQrCode = common_vendor.ref(false);
|
||||
const cartStore = store_cart.useCartStore();
|
||||
const statusBarHeight = common_vendor.ref(20);
|
||||
const navBarHeight = common_vendor.ref(44);
|
||||
try {
|
||||
const sysInfo = common_vendor.index.getSystemInfoSync();
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 20;
|
||||
const menuBtn = common_vendor.index.getMenuButtonBoundingClientRect();
|
||||
navBarHeight.value = (menuBtn.top - (sysInfo.statusBarHeight || 20)) * 2 + menuBtn.height;
|
||||
} catch {
|
||||
}
|
||||
function goBack() {
|
||||
common_vendor.index.navigateBack({ delta: 1 });
|
||||
}
|
||||
function fullUrl(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
if (path.startsWith("http"))
|
||||
return path;
|
||||
return utils_request.BASE_URL + path;
|
||||
}
|
||||
function goCart() {
|
||||
common_vendor.index.switchTab({ url: "/pages/cart/index" });
|
||||
}
|
||||
common_vendor.onMounted(() => {
|
||||
var _a;
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const id = Number((_a = currentPage.options) == null ? void 0 : _a.id);
|
||||
if (id) {
|
||||
if (id)
|
||||
loadProduct(id);
|
||||
}
|
||||
});
|
||||
async function loadProduct(id) {
|
||||
try {
|
||||
|
|
@ -32,76 +53,47 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
|
|||
common_vendor.index.showToast({ title: "加载商品失败", icon: "none" });
|
||||
}
|
||||
}
|
||||
function handleAddToCart() {
|
||||
if (!product.value)
|
||||
return;
|
||||
const item = {
|
||||
id: Date.now(),
|
||||
userId: 0,
|
||||
productId: product.value.id,
|
||||
specDataId: 0,
|
||||
quantity: 1,
|
||||
product: product.value,
|
||||
specData: {
|
||||
id: 0,
|
||||
productId: product.value.id,
|
||||
modelName: product.value.name,
|
||||
fineness: "",
|
||||
mainStone: "",
|
||||
ringSize: "",
|
||||
goldTotalWeight: 0,
|
||||
goldNetWeight: 0,
|
||||
loss: 0,
|
||||
goldLoss: 0,
|
||||
goldPrice: 0,
|
||||
goldValue: 0,
|
||||
mainStoneCount: 0,
|
||||
mainStoneWeight: 0,
|
||||
mainStoneUnitPrice: 0,
|
||||
mainStoneAmount: 0,
|
||||
sideStoneCount: 0,
|
||||
sideStoneWeight: 0,
|
||||
sideStoneUnitPrice: 0,
|
||||
sideStoneAmount: 0,
|
||||
accessoryAmount: 0,
|
||||
processingFee: 0,
|
||||
settingFee: 0,
|
||||
totalLaborCost: 0,
|
||||
totalPrice: product.value.basePrice
|
||||
},
|
||||
checked: true
|
||||
};
|
||||
cartStore.addToCart(item);
|
||||
common_vendor.index.showToast({ title: "已加入购物车", icon: "success" });
|
||||
}
|
||||
return (_ctx, _cache) => {
|
||||
return common_vendor.e({
|
||||
a: product.value
|
||||
}, product.value ? common_vendor.e({
|
||||
b: common_vendor.p({
|
||||
images: product.value.bannerImages,
|
||||
b: common_assets._imports_0$1,
|
||||
c: common_vendor.o(goBack),
|
||||
d: navBarHeight.value + "px",
|
||||
e: statusBarHeight.value + "px",
|
||||
f: statusBarHeight.value + navBarHeight.value + "px",
|
||||
g: common_vendor.p({
|
||||
images: product.value.bannerImages || [],
|
||||
video: product.value.bannerVideo
|
||||
}),
|
||||
c: common_vendor.t(product.value.basePrice),
|
||||
d: common_vendor.t(product.value.name),
|
||||
e: common_vendor.t(product.value.styleNo),
|
||||
f: common_vendor.t(product.value.stock),
|
||||
g: common_vendor.t(product.value.loss),
|
||||
h: common_vendor.t(product.value.laborCost),
|
||||
i: common_vendor.o(($event) => showSpecPanel.value = true),
|
||||
j: showSpecPanel.value
|
||||
h: common_vendor.t(product.value.name),
|
||||
i: common_vendor.t(product.value.basePrice),
|
||||
j: common_vendor.t(product.value.styleNo),
|
||||
k: common_vendor.t(product.value.stock),
|
||||
l: common_vendor.t(product.value.loss),
|
||||
m: common_vendor.t(product.value.laborCost),
|
||||
n: common_vendor.f(product.value.bannerImages || [], (img, idx, i0) => {
|
||||
return {
|
||||
a: idx,
|
||||
b: fullUrl(img)
|
||||
};
|
||||
}),
|
||||
o: common_assets._imports_2,
|
||||
p: common_vendor.o(($event) => showQrCode.value = true),
|
||||
q: common_assets._imports_2$1,
|
||||
r: common_vendor.o(goCart),
|
||||
s: common_vendor.o(($event) => showSpecPanel.value = true),
|
||||
t: showSpecPanel.value
|
||||
}, showSpecPanel.value ? {
|
||||
k: common_vendor.o(($event) => showSpecPanel.value = false),
|
||||
l: common_vendor.p({
|
||||
v: common_vendor.o(($event) => showSpecPanel.value = false),
|
||||
w: common_vendor.p({
|
||||
["product-id"]: product.value.id
|
||||
})
|
||||
} : {}, {
|
||||
m: common_vendor.o(($event) => showQrCode.value = true),
|
||||
n: common_vendor.o(handleAddToCart),
|
||||
o: showQrCode.value
|
||||
x: showQrCode.value
|
||||
}, showQrCode.value ? {
|
||||
p: common_vendor.o(($event) => showQrCode.value = false),
|
||||
q: common_vendor.p({
|
||||
y: common_vendor.o(($event) => showQrCode.value = false),
|
||||
z: common_vendor.p({
|
||||
mode: "qrcode"
|
||||
})
|
||||
} : {}) : {});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"navigationBarTitleText": "商品详情",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"banner-swiper": "../../components/BannerSwiper",
|
||||
"shipping-notice": "../../components/ShippingNotice",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<view wx:if="{{a}}" class="product-detail data-v-acf502d9"><banner-swiper wx:if="{{b}}" class="data-v-acf502d9" u-i="acf502d9-0" bind:__l="__l" u-p="{{b}}"/><view class="base-info data-v-acf502d9"><text class="base-info__price data-v-acf502d9">¥{{c}}</text><text class="base-info__name data-v-acf502d9">{{d}}</text><view class="base-info__attrs data-v-acf502d9"><view class="attr-row data-v-acf502d9"><text class="attr-label data-v-acf502d9">款号</text><text class="attr-value data-v-acf502d9">{{e}}</text></view><view class="attr-row data-v-acf502d9"><text class="attr-label data-v-acf502d9">库存</text><text class="attr-value data-v-acf502d9">{{f}}</text></view><view class="attr-row data-v-acf502d9"><text class="attr-label data-v-acf502d9">损耗</text><text class="attr-value data-v-acf502d9">{{g}}</text></view><view class="attr-row data-v-acf502d9"><text class="attr-label data-v-acf502d9">工费</text><text class="attr-value data-v-acf502d9">¥{{h}}</text></view></view></view><shipping-notice class="data-v-acf502d9" u-i="acf502d9-1" bind:__l="__l"/><view class="spec-entry data-v-acf502d9" bindtap="{{i}}"><text class="data-v-acf502d9">查看详细参数</text><text class="spec-entry__arrow data-v-acf502d9">›</text></view><spec-panel wx:if="{{j}}" class="data-v-acf502d9" bindclose="{{k}}" u-i="acf502d9-2" bind:__l="__l" u-p="{{l}}"/><view class="bottom-bar data-v-acf502d9"><view class="bottom-bar__btn bottom-bar__btn--cs data-v-acf502d9" bindtap="{{m}}">客服</view><view class="bottom-bar__btn bottom-bar__btn--cart data-v-acf502d9" bindtap="{{n}}">加入购物车</view></view><customer-service-btn wx:if="{{o}}" class="data-v-acf502d9" bindclose="{{p}}" u-i="acf502d9-3" bind:__l="__l" u-p="{{q}}"/></view>
|
||||
<view wx:if="{{a}}" class="product-detail data-v-acf502d9"><view class="custom-navbar data-v-acf502d9" style="{{'padding-top:' + e}}"><view class="custom-navbar__content data-v-acf502d9" style="{{'height:' + d}}"><image class="custom-navbar__back data-v-acf502d9" src="{{b}}" mode="aspectFit" bindtap="{{c}}"/><text class="custom-navbar__title data-v-acf502d9">商品详情</text><view class="custom-navbar__placeholder data-v-acf502d9"/></view></view><view class="data-v-acf502d9" style="{{'height:' + f}}"/><view class="banner-wrapper data-v-acf502d9"><banner-swiper wx:if="{{g}}" class="data-v-acf502d9" u-i="acf502d9-0" bind:__l="__l" u-p="{{g}}"/></view><view class="base-info data-v-acf502d9"><view class="base-info__top data-v-acf502d9"><text class="base-info__name data-v-acf502d9">{{h}}</text><view class="base-info__price data-v-acf502d9"><text class="base-info__price-symbol data-v-acf502d9">¥</text><text class="base-info__price-num data-v-acf502d9">{{i}}</text><text class="base-info__price-unit data-v-acf502d9">元</text></view></view><view class="base-info__attrs data-v-acf502d9"><view class="attr-row data-v-acf502d9"><text class="attr-label data-v-acf502d9">款 号</text><text class="attr-value data-v-acf502d9">{{j}}</text><text class="attr-label data-v-acf502d9">库存</text><text class="attr-value data-v-acf502d9">{{k}}</text></view><view class="attr-row data-v-acf502d9"><text class="attr-label data-v-acf502d9">损 耗</text><text class="attr-value data-v-acf502d9">{{l}}%</text><text class="attr-label data-v-acf502d9">工费</text><text class="attr-value data-v-acf502d9">¥{{m}}</text></view></view></view><shipping-notice class="data-v-acf502d9" u-i="acf502d9-1" bind:__l="__l"/><view class="detail-section data-v-acf502d9"><view class="detail-section__title data-v-acf502d9">商品详情</view><view class="detail-section__images data-v-acf502d9"><image wx:for="{{n}}" wx:for-item="img" wx:key="a" class="detail-section__img data-v-acf502d9" src="{{img.b}}" mode="widthFix"/></view></view><view class="bottom-bar data-v-acf502d9"><view class="bottom-bar__icons data-v-acf502d9"><view class="bottom-bar__icon-item data-v-acf502d9" bindtap="{{p}}"><image class="bottom-bar__icon-img data-v-acf502d9" src="{{o}}" mode="aspectFit"/><text class="bottom-bar__icon-text data-v-acf502d9">客服</text></view><view class="bottom-bar__icon-item data-v-acf502d9" bindtap="{{r}}"><image class="bottom-bar__icon-img data-v-acf502d9" src="{{q}}" mode="aspectFit"/><text class="bottom-bar__icon-text data-v-acf502d9">购物车</text></view></view><view class="bottom-bar__main-btn data-v-acf502d9" bindtap="{{s}}"><text class="data-v-acf502d9">空托—查看详细参数</text></view></view><spec-panel wx:if="{{t}}" class="data-v-acf502d9" bindclose="{{v}}" u-i="acf502d9-2" bind:__l="__l" u-p="{{w}}"/><customer-service-btn wx:if="{{x}}" class="data-v-acf502d9" bindclose="{{y}}" u-i="acf502d9-3" bind:__l="__l" u-p="{{z}}"/></view>
|
||||
|
|
@ -1,54 +1,135 @@
|
|||
|
||||
.product-detail.data-v-acf502d9 {
|
||||
padding-bottom: 120rpx;
|
||||
padding-bottom: 140rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar.data-v-acf502d9 {
|
||||
background: linear-gradient(to right, #FFCFDE, #FFA6C4);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.custom-navbar__content.data-v-acf502d9 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
.custom-navbar__back.data-v-acf502d9 {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
.custom-navbar__title.data-v-acf502d9 {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.custom-navbar__placeholder.data-v-acf502d9 {
|
||||
width: 44rpx;
|
||||
}
|
||||
|
||||
/* Banner 容器 */
|
||||
.banner-wrapper.data-v-acf502d9 {
|
||||
margin: 20rpx 24rpx 0;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 基础信息卡片 */
|
||||
.base-info.data-v-acf502d9 {
|
||||
background: #fff;
|
||||
padding: 24rpx;
|
||||
margin: 20rpx 24rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.base-info__price.data-v-acf502d9 {
|
||||
font-size: 40rpx;
|
||||
color: #e4393c;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
.base-info__top.data-v-acf502d9 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.base-info__name.data-v-acf502d9 {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-top: 12rpx;
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.base-info__price.data-v-acf502d9 {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: #FF6D9B;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.base-info__price-symbol.data-v-acf502d9 {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.base-info__price-num.data-v-acf502d9 {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.base-info__price-unit.data-v-acf502d9 {
|
||||
font-size: 24rpx;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
/* 属性行 - 两列布局 */
|
||||
.base-info__attrs.data-v-acf502d9 {
|
||||
margin-top: 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
.attr-row.data-v-acf502d9 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
.attr-label.data-v-acf502d9 {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
width: 80rpx;
|
||||
flex-shrink: 0;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
.attr-value.data-v-acf502d9 {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
.spec-entry.data-v-acf502d9 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
margin-top: 16rpx;
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
min-width: 180rpx;
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
.spec-entry__arrow.data-v-acf502d9 {
|
||||
color: #999;
|
||||
font-size: 32rpx;
|
||||
|
||||
/* 商品详情大图 */
|
||||
.detail-section.data-v-acf502d9 {
|
||||
background: #fff;
|
||||
margin: 20rpx 24rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.detail-section__title.data-v-acf502d9 {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.detail-section__images.data-v-acf502d9 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.detail-section__img.data-v-acf502d9 {
|
||||
width: 100%;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-bar.data-v-acf502d9 {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
|
@ -56,24 +137,38 @@
|
|||
right: 0;
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.bottom-bar__btn.data-v-acf502d9 {
|
||||
.bottom-bar__icons.data-v-acf502d9 {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bottom-bar__icon-item.data-v-acf502d9 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.bottom-bar__icon-img.data-v-acf502d9 {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
.bottom-bar__icon-text.data-v-acf502d9 {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
.bottom-bar__main-btn.data-v-acf502d9 {
|
||||
flex: 1;
|
||||
background: linear-gradient(to right, #f5a0b8, #e4393c);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
padding: 24rpx 0;
|
||||
border-radius: 44rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.bottom-bar__btn--cs.data-v-acf502d9 {
|
||||
flex: 1;
|
||||
border: 1rpx solid #ddd;
|
||||
color: #666;
|
||||
background: #fff;
|
||||
}
|
||||
.bottom-bar__btn--cart.data-v-acf502d9 {
|
||||
flex: 2;
|
||||
background: #e4393c;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
|
|||
BIN
miniprogram/unpackage/dist/dev/mp-weixin/static/ic_back.png
vendored
Normal file
BIN
miniprogram/unpackage/dist/dev/mp-weixin/static/ic_back.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 648 B |
BIN
miniprogram/unpackage/dist/dev/mp-weixin/static/ic_notice.png
vendored
Normal file
BIN
miniprogram/unpackage/dist/dev/mp-weixin/static/ic_notice.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
miniprogram/unpackage/dist/dev/mp-weixin/static/ic_search.png
vendored
Normal file
BIN
miniprogram/unpackage/dist/dev/mp-weixin/static/ic_search.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,5 +1,6 @@
|
|||
"use strict";
|
||||
const common_vendor = require("../common/vendor.js");
|
||||
const api_cart = require("../api/cart.js");
|
||||
const useCartStore = common_vendor.defineStore("cart", () => {
|
||||
const items = common_vendor.ref([]);
|
||||
const checkedItems = common_vendor.computed(() => items.value.filter((item) => item.checked));
|
||||
|
|
@ -8,21 +9,17 @@ const useCartStore = common_vendor.defineStore("cart", () => {
|
|||
);
|
||||
async function fetchCart() {
|
||||
try {
|
||||
const { getCartList } = await "../api/cart.js";
|
||||
const list = await getCartList();
|
||||
const list = await api_cart.getCartList();
|
||||
items.value = list.map((item) => ({ ...item, checked: true }));
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
function addToCart(item) {
|
||||
items.value.push(item);
|
||||
"../api/cart.js".then(({ addToCart: apiAdd }) => {
|
||||
apiAdd({
|
||||
productId: item.productId,
|
||||
specDataId: item.specDataId,
|
||||
quantity: item.quantity
|
||||
}).catch(() => {
|
||||
});
|
||||
api_cart.addToCart({
|
||||
productId: item.productId,
|
||||
specDataId: item.specDataId,
|
||||
quantity: item.quantity
|
||||
}).catch(() => {
|
||||
});
|
||||
}
|
||||
|
|
@ -30,10 +27,7 @@ const useCartStore = common_vendor.defineStore("cart", () => {
|
|||
const index = items.value.findIndex((item) => item.id === id);
|
||||
if (index !== -1) {
|
||||
items.value.splice(index, 1);
|
||||
"../api/cart.js".then(({ deleteCartItem }) => {
|
||||
deleteCartItem(id).catch(() => {
|
||||
});
|
||||
}).catch(() => {
|
||||
api_cart.deleteCartItem(id).catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -41,10 +35,7 @@ const useCartStore = common_vendor.defineStore("cart", () => {
|
|||
const item = items.value.find((item2) => item2.id === id);
|
||||
if (item) {
|
||||
item.quantity = quantity;
|
||||
"../api/cart.js".then(({ updateCartItem }) => {
|
||||
updateCartItem(id, { quantity }).catch(() => {
|
||||
});
|
||||
}).catch(() => {
|
||||
api_cart.updateCartItem(id, { quantity }).catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,16 +66,16 @@ function autoLogin() {
|
|||
common_vendor.index.setStorageSync("token", data.token);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
common_vendor.index.__f__("error", "at utils/request.ts:87", "登录接口调用失败:", err);
|
||||
common_vendor.index.__f__("error", "at utils/request.ts:89", "登录接口调用失败:", err);
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
common_vendor.index.__f__("error", "at utils/request.ts:91", "微信登录获取 code 失败");
|
||||
common_vendor.index.__f__("error", "at utils/request.ts:93", "微信登录获取 code 失败");
|
||||
reject(new Error("获取微信 code 失败"));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
common_vendor.index.__f__("error", "at utils/request.ts:96", "uni.login 失败:", err);
|
||||
common_vendor.index.__f__("error", "at utils/request.ts:98", "uni.login 失败:", err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
|
@ -85,6 +85,7 @@ const get = (url, data) => request({ url, method: "GET", data });
|
|||
const post = (url, data) => request({ url, method: "POST", data });
|
||||
const put = (url, data) => request({ url, method: "PUT", data });
|
||||
const del = (url, data) => request({ url, method: "DELETE", data });
|
||||
exports.BASE_URL = BASE_URL;
|
||||
exports.autoLogin = autoLogin;
|
||||
exports.del = del;
|
||||
exports.get = get;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ const ENV_BASE_URL: Record<string, string> = {
|
|||
|
||||
const BASE_URL = ENV_BASE_URL[process.env.NODE_ENV || 'development'] || 'http://localhost:3000'
|
||||
|
||||
export { BASE_URL }
|
||||
|
||||
interface RequestOptions {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS products (
|
|||
category_id INT DEFAULT NULL,
|
||||
banner_images JSON DEFAULT NULL,
|
||||
banner_video VARCHAR(512) DEFAULT NULL,
|
||||
thumb VARCHAR(512) DEFAULT NULL,
|
||||
status ENUM('on','off') NOT NULL DEFAULT 'on',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
|
|
|||
55
server/src/controllers/adminCategory.ts
Normal file
55
server/src/controllers/adminCategory.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Request, Response } from 'express'
|
||||
import pool from '../utils/db'
|
||||
import { RowDataPacket, ResultSetHeader } from 'mysql2'
|
||||
|
||||
// GET /api/admin/categories
|
||||
export async function adminGetCategories(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT id, name, icon, parent_id, sort FROM categories ORDER BY sort ASC, id ASC'
|
||||
)
|
||||
res.json({ code: 0, data: rows })
|
||||
} catch (err) {
|
||||
console.error('adminGetCategories error:', err)
|
||||
res.status(500).json({ code: 500, message: '获取分类列表失败' })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/admin/categories
|
||||
export async function adminCreateCategory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { name, parentId, sort, icon } = req.body
|
||||
if (!name || !name.trim()) {
|
||||
res.status(400).json({ code: 400, message: '分类名称不能为空' })
|
||||
return
|
||||
}
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
'INSERT INTO categories (name, icon, parent_id, sort) VALUES (?, ?, ?, ?)',
|
||||
[name.trim(), icon || null, parentId || null, sort ?? 0]
|
||||
)
|
||||
res.json({ code: 0, data: { id: result.insertId } })
|
||||
} catch (err) {
|
||||
console.error('adminCreateCategory error:', err)
|
||||
res.status(500).json({ code: 500, message: '创建分类失败' })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/admin/categories/:id
|
||||
export async function adminDeleteCategory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params
|
||||
// Check if any products use this category
|
||||
const [products] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT COUNT(*) as count FROM products WHERE category_id = ?', [id]
|
||||
)
|
||||
if (products[0].count > 0) {
|
||||
res.status(400).json({ code: 400, message: '该分类下有商品,无法删除' })
|
||||
return
|
||||
}
|
||||
await pool.execute('DELETE FROM categories WHERE id = ?', [id])
|
||||
res.json({ code: 0, message: '删除成功' })
|
||||
} catch (err) {
|
||||
console.error('adminDeleteCategory error:', err)
|
||||
res.status(500).json({ code: 500, message: '删除分类失败' })
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export async function adminGetProductDetail(req: Request, res: Response): Promis
|
|||
const { id } = req.params
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT id, name, base_price, style_no, stock, total_stock, loss, labor_cost,
|
||||
category_id, banner_images, banner_video, status, created_at, updated_at
|
||||
category_id, banner_images, banner_video, thumb, status, created_at, updated_at
|
||||
FROM products WHERE id = ?`,
|
||||
[id]
|
||||
)
|
||||
|
|
@ -71,7 +71,7 @@ export async function adminGetProductDetail(req: Request, res: Response): Promis
|
|||
export async function adminCreateProduct(req: Request, res: Response): Promise<void> {
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
const { name, basePrice, styleNo, stock, totalStock, loss, laborCost, categoryId, bannerImages, bannerVideo, status, detailParams } = req.body
|
||||
const { name, basePrice, styleNo, stock, totalStock, loss, laborCost, categoryId, bannerImages, bannerVideo, thumb, status, detailParams } = req.body
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({ code: 400, message: '商品名称不能为空' })
|
||||
|
|
@ -81,9 +81,9 @@ export async function adminCreateProduct(req: Request, res: Response): Promise<v
|
|||
await conn.beginTransaction()
|
||||
|
||||
const [result] = await conn.execute<ResultSetHeader>(
|
||||
`INSERT INTO products (name, base_price, style_no, stock, total_stock, loss, labor_cost, category_id, banner_images, banner_video, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[name, basePrice || 0, styleNo || '', stock || 0, totalStock || 0, loss || 0, laborCost || 0, categoryId || null, JSON.stringify(bannerImages || []), bannerVideo || null, status || 'on']
|
||||
`INSERT INTO products (name, base_price, style_no, stock, total_stock, loss, labor_cost, category_id, banner_images, banner_video, thumb, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[name, basePrice || 0, styleNo || '', stock || 0, totalStock || 0, loss || 0, laborCost || 0, categoryId || null, JSON.stringify(bannerImages || []), bannerVideo || null, thumb || null, status || 'on']
|
||||
)
|
||||
const productId = result.insertId
|
||||
|
||||
|
|
@ -111,14 +111,14 @@ export async function adminUpdateProduct(req: Request, res: Response): Promise<v
|
|||
const conn = await pool.getConnection()
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { name, basePrice, styleNo, stock, totalStock, loss, laborCost, categoryId, bannerImages, bannerVideo, status, detailParams } = req.body
|
||||
const { name, basePrice, styleNo, stock, totalStock, loss, laborCost, categoryId, bannerImages, bannerVideo, thumb, status, detailParams } = req.body
|
||||
|
||||
await conn.beginTransaction()
|
||||
|
||||
await conn.execute(
|
||||
`UPDATE products SET name=?, base_price=?, style_no=?, stock=?, total_stock=?, loss=?, labor_cost=?, category_id=?, banner_images=?, banner_video=?, status=?
|
||||
`UPDATE products SET name=?, base_price=?, style_no=?, stock=?, total_stock=?, loss=?, labor_cost=?, category_id=?, banner_images=?, banner_video=?, thumb=?, status=?
|
||||
WHERE id=?`,
|
||||
[name, basePrice || 0, styleNo || '', stock || 0, totalStock || 0, loss || 0, laborCost || 0, categoryId || null, JSON.stringify(bannerImages || []), bannerVideo || null, status || 'on', id]
|
||||
[name, basePrice || 0, styleNo || '', stock || 0, totalStock || 0, loss || 0, laborCost || 0, categoryId || null, JSON.stringify(bannerImages || []), bannerVideo || null, thumb || null, status || 'on', id]
|
||||
)
|
||||
|
||||
// Update detail parameter config
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export async function getProducts(req: Request, res: Response): Promise<void> {
|
|||
const total = countRows[0].total
|
||||
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT id, name, base_price, style_no, stock, banner_images, status
|
||||
`SELECT id, name, base_price AS basePrice, style_no AS styleNo, stock, banner_images AS bannerImages, thumb, status
|
||||
FROM products ${where}
|
||||
ORDER BY id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, String(pageSize), String(offset)]
|
||||
|
|
@ -43,8 +43,10 @@ export async function getProductDetail(req: Request, res: Response): Promise<voi
|
|||
try {
|
||||
const { id } = req.params
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT id, name, base_price, style_no, stock, total_stock, loss, labor_cost,
|
||||
category_id, banner_images, banner_video, status, created_at, updated_at
|
||||
`SELECT id, name, base_price AS basePrice, style_no AS styleNo, stock, total_stock AS totalStock,
|
||||
loss, labor_cost AS laborCost, category_id AS categoryId,
|
||||
banner_images AS bannerImages, banner_video AS bannerVideo, thumb, status,
|
||||
created_at AS createdAt, updated_at AS updatedAt
|
||||
FROM products WHERE id = ?`,
|
||||
[id]
|
||||
)
|
||||
|
|
@ -66,7 +68,7 @@ export async function getProductSpecs(req: Request, res: Response): Promise<void
|
|||
try {
|
||||
const { id } = req.params
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT id, product_id, fineness, main_stone, ring_size FROM detail_parameter_configs WHERE product_id = ?',
|
||||
'SELECT id, product_id AS productId, fineness, main_stone AS mainStone, ring_size AS ringSize FROM detail_parameter_configs WHERE product_id = ?',
|
||||
[id]
|
||||
)
|
||||
|
||||
|
|
@ -100,7 +102,16 @@ export async function getSpecData(req: Request, res: Response): Promise<void> {
|
|||
}
|
||||
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT * FROM spec_data ${where}`,
|
||||
`SELECT id, product_id AS productId, model_name AS modelName, fineness, main_stone AS mainStone,
|
||||
ring_size AS ringSize, gold_total_weight AS goldTotalWeight, gold_net_weight AS goldNetWeight,
|
||||
loss, gold_loss AS goldLoss, gold_price AS goldPrice, gold_value AS goldValue,
|
||||
main_stone_count AS mainStoneCount, main_stone_weight AS mainStoneWeight,
|
||||
main_stone_unit_price AS mainStoneUnitPrice, main_stone_amount AS mainStoneAmount,
|
||||
side_stone_count AS sideStoneCount, side_stone_weight AS sideStoneWeight,
|
||||
side_stone_unit_price AS sideStoneUnitPrice, side_stone_amount AS sideStoneAmount,
|
||||
accessory_amount AS accessoryAmount, processing_fee AS processingFee,
|
||||
setting_fee AS settingFee, total_labor_cost AS totalLaborCost, total_price AS totalPrice
|
||||
FROM spec_data ${where}`,
|
||||
params
|
||||
)
|
||||
|
||||
|
|
@ -115,7 +126,7 @@ export async function getSpecData(req: Request, res: Response): Promise<void> {
|
|||
export async function getCategories(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT id, name, parent_id, sort FROM categories ORDER BY sort ASC'
|
||||
'SELECT id, name, icon, parent_id AS parentId, sort FROM categories ORDER BY sort ASC'
|
||||
)
|
||||
res.json({ code: 0, data: rows })
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,68 @@ export function generateCSV(rows: Record<string, any>[]): string {
|
|||
return [headerLine, ...dataLines].join('\n')
|
||||
}
|
||||
|
||||
// GET /api/admin/products/:id/spec-data - 获取规格数据列表
|
||||
export async function adminGetSpecData(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT id, product_id AS productId, model_name AS modelName, fineness, main_stone AS mainStone,
|
||||
ring_size AS ringSize, gold_total_weight AS goldTotalWeight, gold_net_weight AS goldNetWeight,
|
||||
loss, gold_loss AS goldLoss, gold_price AS goldPrice, gold_value AS goldValue,
|
||||
main_stone_count AS mainStoneCount, main_stone_weight AS mainStoneWeight,
|
||||
main_stone_unit_price AS mainStoneUnitPrice, main_stone_amount AS mainStoneAmount,
|
||||
side_stone_count AS sideStoneCount, side_stone_weight AS sideStoneWeight,
|
||||
side_stone_unit_price AS sideStoneUnitPrice, side_stone_amount AS sideStoneAmount,
|
||||
accessory_amount AS accessoryAmount, processing_fee AS processingFee,
|
||||
setting_fee AS settingFee, total_labor_cost AS totalLaborCost, total_price AS totalPrice
|
||||
FROM spec_data WHERE product_id = ? ORDER BY id ASC`,
|
||||
[id]
|
||||
)
|
||||
res.json({ code: 0, data: rows })
|
||||
} catch (err) {
|
||||
console.error('adminGetSpecData error:', err)
|
||||
res.status(500).json({ code: 500, message: '获取规格数据失败' })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/admin/products/:id/spec-data - 新增单条规格数据
|
||||
export async function adminCreateSpecData(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const d = req.body
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
`INSERT INTO spec_data (product_id, model_name, fineness, main_stone, ring_size,
|
||||
gold_total_weight, gold_net_weight, loss, gold_loss, gold_price, gold_value,
|
||||
main_stone_count, main_stone_weight, main_stone_unit_price, main_stone_amount,
|
||||
side_stone_count, side_stone_weight, side_stone_unit_price, side_stone_amount,
|
||||
accessory_amount, processing_fee, setting_fee, total_labor_cost, total_price)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[id, d.modelName||'', d.fineness||'', d.mainStone||'', d.ringSize||'',
|
||||
d.goldTotalWeight||0, d.goldNetWeight||0, d.loss||0, d.goldLoss||0,
|
||||
d.goldPrice||0, d.goldValue||0,
|
||||
d.mainStoneCount||0, d.mainStoneWeight||0, d.mainStoneUnitPrice||0, d.mainStoneAmount||0,
|
||||
d.sideStoneCount||0, d.sideStoneWeight||0, d.sideStoneUnitPrice||0, d.sideStoneAmount||0,
|
||||
d.accessoryAmount||0, d.processingFee||0, d.settingFee||0, d.totalLaborCost||0, d.totalPrice||0]
|
||||
)
|
||||
res.json({ code: 0, data: { id: result.insertId } })
|
||||
} catch (err) {
|
||||
console.error('adminCreateSpecData error:', err)
|
||||
res.status(500).json({ code: 500, message: '新增规格数据失败' })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/admin/products/:productId/spec-data/:specId
|
||||
export async function adminDeleteSpecData(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { specId } = req.params
|
||||
await pool.execute('DELETE FROM spec_data WHERE id = ?', [specId])
|
||||
res.json({ code: 0, message: '删除成功' })
|
||||
} catch (err) {
|
||||
console.error('adminDeleteSpecData error:', err)
|
||||
res.status(500).json({ code: 500, message: '删除规格数据失败' })
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/admin/products/:id/spec-data/export
|
||||
export async function exportSpecData(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
adminUpdateProduct,
|
||||
adminDeleteProduct,
|
||||
} from '../controllers/adminProduct'
|
||||
import { exportSpecData, importSpecData } from '../controllers/specDataIO'
|
||||
import { exportSpecData, importSpecData, adminGetSpecData, adminCreateSpecData, adminDeleteSpecData } from '../controllers/specDataIO'
|
||||
import { getStockAlerts } from '../controllers/stockAlert'
|
||||
import {
|
||||
adminGetOrders,
|
||||
|
|
@ -24,6 +24,11 @@ import {
|
|||
adminUpdateMold,
|
||||
adminDeleteMold,
|
||||
} from '../controllers/adminMold'
|
||||
import {
|
||||
adminGetCategories,
|
||||
adminCreateCategory,
|
||||
adminDeleteCategory,
|
||||
} from '../controllers/adminCategory'
|
||||
|
||||
const csvUpload = multer({ storage: multer.memoryStorage() })
|
||||
|
||||
|
|
@ -45,9 +50,12 @@ adminRoutes.post('/products', adminCreateProduct)
|
|||
adminRoutes.put('/products/:id', adminUpdateProduct)
|
||||
adminRoutes.delete('/products/:id', adminDeleteProduct)
|
||||
|
||||
// Spec data import/export
|
||||
// Spec data CRUD + import/export
|
||||
adminRoutes.get('/products/:id/spec-data/export', exportSpecData)
|
||||
adminRoutes.post('/products/:id/spec-data/import', csvUpload.single('file'), importSpecData)
|
||||
adminRoutes.get('/products/:id/spec-data', adminGetSpecData)
|
||||
adminRoutes.post('/products/:id/spec-data', adminCreateSpecData)
|
||||
adminRoutes.delete('/products/:productId/spec-data/:specId', adminDeleteSpecData)
|
||||
|
||||
// Stock alerts
|
||||
adminRoutes.get('/stock-alerts', getStockAlerts)
|
||||
|
|
@ -64,3 +72,8 @@ adminRoutes.get('/molds', adminGetMolds)
|
|||
adminRoutes.post('/molds', adminCreateMold)
|
||||
adminRoutes.put('/molds/:id', adminUpdateMold)
|
||||
adminRoutes.delete('/molds/:id', adminDeleteMold)
|
||||
|
||||
// Category management
|
||||
adminRoutes.get('/categories', adminGetCategories)
|
||||
adminRoutes.post('/categories', adminCreateCategory)
|
||||
adminRoutes.delete('/categories/:id', adminDeleteCategory)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const pool = mysql.createPool({
|
|||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
charset: 'utf8mb4',
|
||||
})
|
||||
|
||||
export default pool
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user