新增摸大红业务并修复后台链接按钮白字

支持分类筛选、搜索、下单支付建群与固定二维码;合并迁移种子数据;后台表格编辑/详情链接恢复主题色可见。
This commit is contained in:
yml2213
2026-07-16 18:08:18 +08:00
parent 97a34e0329
commit 6d61027318
49 changed files with 9533 additions and 20 deletions
@@ -0,0 +1,184 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh } from '@element-plus/icons-vue'
import {
createAdminMohongCategory,
deleteAdminMohongCategory,
fetchAdminMohongCategories,
updateAdminMohongCategory,
type MohongCategory,
} from '@/features/mohong/api/mohong'
import { readError } from '@/shared/utils/error'
const loading = ref(false)
const items = ref<MohongCategory[]>([])
const dialogVisible = ref(false)
const saving = ref(false)
const editingId = ref<number | null>(null)
const form = reactive({
name: '',
code: '',
sort_order: 0,
status: 'enabled',
})
onMounted(load)
async function load() {
loading.value = true
try {
items.value = await fetchAdminMohongCategories()
} catch (error) {
ElMessage.error(readError(error, '加载失败'))
} finally {
loading.value = false
}
}
function openCreate() {
editingId.value = null
Object.assign(form, { name: '', code: '', sort_order: 0, status: 'enabled' })
dialogVisible.value = true
}
function openEdit(item: MohongCategory) {
editingId.value = item.id
Object.assign(form, {
name: item.name,
code: item.code || '',
sort_order: item.sort_order,
status: item.status,
})
dialogVisible.value = true
}
async function handleSave() {
if (!form.name.trim()) {
ElMessage.warning('请填写分类名称')
return
}
saving.value = true
try {
if (editingId.value) {
await updateAdminMohongCategory(editingId.value, { ...form })
ElMessage.success('已更新')
} else {
await createAdminMohongCategory({ ...form })
ElMessage.success('已创建')
}
dialogVisible.value = false
await load()
} catch (error) {
ElMessage.error(readError(error, '保存失败'))
} finally {
saving.value = false
}
}
async function handleDelete(item: MohongCategory) {
try {
await ElMessageBox.confirm(
`确认删除分类「${item.name}」?其下商品将变为未分类。`,
'提示',
{ type: 'warning' }
)
await deleteAdminMohongCategory(item.id)
ElMessage.success('已删除')
await load()
} catch (error) {
if (error !== 'cancel') ElMessage.error(readError(error, '删除失败'))
}
}
</script>
<template>
<div class="admin-page">
<div class="page-head">
<div>
<h2>摸大红分类</h2>
<p>大红 / 四格大红 / 炫彩等分类前台左侧筛选使用</p>
</div>
<div class="actions">
<el-button :icon="Refresh" @click="load">刷新</el-button>
<el-button type="primary" :icon="Plus" @click="openCreate">新建分类</el-button>
</div>
</div>
<el-table v-loading="loading" :data="items" border stripe>
<el-table-column prop="name" label="名称" min-width="140" />
<el-table-column prop="code" label="编码" width="140" />
<el-table-column prop="sort_order" label="排序" width="90" />
<el-table-column label="商品数" width="90">
<template #default="{ row }">{{ row.product_count }}</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
{{ row.status === 'enabled' ? '启用' : '停用' }}
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="dialogVisible"
:title="editingId ? '编辑分类' : '新建分类'"
width="480px"
destroy-on-close
>
<el-form label-width="80px">
<el-form-item label="名称" required>
<el-input v-model="form.name" maxlength="64" />
</el-form-item>
<el-form-item label="编码">
<el-input v-model="form.code" maxlength="64" placeholder="可选" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort_order" />
<span class="hint">越小越靠前</span>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="form.status" style="width: 160px">
<el-option label="启用" value="enabled" />
<el-option label="停用" value="disabled" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.admin-page {
display: grid;
gap: 16px;
}
.page-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.page-head h2 {
margin: 0 0 4px;
}
.page-head p {
margin: 0;
color: #6b7a90;
font-size: 13px;
}
.hint {
margin-left: 8px;
color: #8a94a6;
font-size: 12px;
}
</style>
@@ -0,0 +1,149 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Upload } from '@element-plus/icons-vue'
import {
fetchAdminMohongConfig,
updateAdminMohongConfig,
type MohongConfig,
} from '@/features/mohong/api/mohong'
import { uploadAdminFile } from '@/shared/api/files'
import { readError } from '@/shared/utils/error'
const loading = ref(false)
const saving = ref(false)
const form = reactive<MohongConfig>({
default_qrcode_url: '',
group_welcome_text: '',
order_copy_template: '',
})
onMounted(load)
async function load() {
loading.value = true
try {
const config = await fetchAdminMohongConfig()
Object.assign(form, config)
} catch (error) {
ElMessage.error(readError(error, '加载配置失败'))
} finally {
loading.value = false
}
}
async function handleSave() {
saving.value = true
try {
const config = await updateAdminMohongConfig({
default_qrcode_url: form.default_qrcode_url,
group_welcome_text: form.group_welcome_text,
order_copy_template: form.order_copy_template,
})
Object.assign(form, config)
ElMessage.success('已保存')
} catch (error) {
ElMessage.error(readError(error, '保存失败'))
} finally {
saving.value = false
}
}
async function uploadQrcode(file: File) {
try {
const uploaded = await uploadAdminFile(file, 'mohong')
form.default_qrcode_url = uploaded.url
ElMessage.success('上传成功')
} catch (error) {
ElMessage.error(readError(error, '上传失败'))
}
return false
}
</script>
<template>
<div v-loading="loading" class="admin-page">
<div class="page-head">
<div>
<h2>摸大红配置</h2>
<p>全局默认固定二维码群欢迎语订单复制模板商品可覆盖默认二维码</p>
</div>
<el-button type="primary" :loading="saving" @click="handleSave">保存配置</el-button>
</div>
<el-form label-width="140px" class="config-form">
<el-form-item label="默认固定二维码">
<div class="qr-row">
<el-image
v-if="form.default_qrcode_url"
:src="form.default_qrcode_url"
fit="contain"
style="width: 160px; height: 160px; border-radius: 12px; background: #f7f9fc"
/>
<div class="qr-actions">
<el-upload :show-file-list="false" accept="image/*" :before-upload="uploadQrcode">
<el-button :icon="Upload">上传二维码</el-button>
</el-upload>
<el-input v-model="form.default_qrcode_url" placeholder="或直接填写图片 URL" />
</div>
</div>
</el-form-item>
<el-form-item label="群欢迎语">
<el-input v-model="form.group_welcome_text" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="复制文案模板">
<el-input v-model="form.order_copy_template" type="textarea" :rows="8" />
<div class="hint">
可用变量&#123;&#123;order_no&#125;&#125; &#123;&#123;created_at&#125;&#125;
&#123;&#123;product_title&#125;&#125; &#123;&#123;quantity&#125;&#125; &#123;&#123;amount&#125;&#125;
&#123;&#123;buyer_name&#125;&#125; &#123;&#123;buyer_phone&#125;&#125; &#123;&#123;unit&#125;&#125;
</div>
</el-form-item>
</el-form>
</div>
</template>
<style scoped>
.admin-page {
display: grid;
gap: 16px;
}
.page-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.page-head h2 {
margin: 0 0 4px;
}
.page-head p {
margin: 0;
color: #6b7a90;
font-size: 13px;
}
.config-form {
max-width: 820px;
padding: 20px;
border-radius: 12px;
background: #fff;
border: 1px solid #eef1f5;
}
.qr-row {
display: flex;
gap: 16px;
align-items: flex-start;
width: 100%;
}
.qr-actions {
flex: 1;
display: grid;
gap: 10px;
}
.hint {
margin-top: 8px;
color: #8a94a6;
font-size: 12px;
line-height: 1.5;
}
</style>
@@ -0,0 +1,244 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import {
cancelAdminMohongOrder,
completeAdminMohongOrder,
fetchAdminMohongOrders,
mohongOrderStatusLabel,
type MohongOrder,
} from '@/features/mohong/api/mohong'
import { formatCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
const items = ref<MohongOrder[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const keyword = ref('')
const status = ref('')
const detail = ref<MohongOrder | null>(null)
onMounted(load)
async function load() {
loading.value = true
try {
const result = await fetchAdminMohongOrders({
keyword: keyword.value || undefined,
status: status.value || undefined,
page: page.value,
page_size: pageSize.value,
})
items.value = result.items || []
total.value = result.total || 0
} catch (error) {
ElMessage.error(readError(error, '加载失败'))
} finally {
loading.value = false
}
}
async function handleComplete(row: MohongOrder) {
try {
await ElMessageBox.confirm(`确认将订单 ${row.order_no} 标记为已完成?`, '提示', {
type: 'warning',
})
await completeAdminMohongOrder(row.id)
ElMessage.success('已完成')
await load()
} catch (error) {
if (error !== 'cancel') ElMessage.error(readError(error, '操作失败'))
}
}
async function handleCancel(row: MohongOrder) {
try {
const { value } = await ElMessageBox.prompt('请输入取消原因', '取消订单', {
inputPlaceholder: '可选',
confirmButtonText: '确认取消',
cancelButtonText: '返回',
})
await cancelAdminMohongOrder(row.id, value || '后台取消')
ElMessage.success('已取消')
await load()
} catch (error) {
if (error !== 'cancel') ElMessage.error(readError(error, '操作失败'))
}
}
async function copyText(text: string) {
try {
await navigator.clipboard.writeText(text)
ElMessage.success('已复制')
} catch {
ElMessage.error('复制失败')
}
}
</script>
<template>
<div class="admin-page">
<div class="page-head">
<div>
<h2>摸大红订单</h2>
<p>查看订单完成履约复制用户订单信息</p>
</div>
<el-button :icon="Refresh" @click="load">刷新</el-button>
</div>
<div class="filters">
<el-input v-model="keyword" clearable placeholder="订单号" style="width: 220px" @keyup.enter="load" />
<el-select v-model="status" clearable placeholder="状态" style="width: 140px">
<el-option label="待支付" value="pending_payment" />
<el-option label="已支付" value="paid" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
<el-button type="primary" @click=";(page = 1), load()">查询</el-button>
</div>
<el-table v-loading="loading" :data="items" border stripe>
<el-table-column prop="order_no" label="订单号" min-width="170" />
<el-table-column prop="product_title" label="商品" min-width="140" />
<el-table-column label="数量" width="70" prop="quantity" />
<el-table-column label="金额" width="100">
<template #default="{ row }">¥{{ row.amount || formatCent(row.amount_cent) }}</template>
</el-table-column>
<el-table-column label="买家" min-width="140">
<template #default="{ row }">
{{ row.buyer_nickname || '-' }}
<div class="sub">{{ row.buyer_phone || '' }}</div>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">{{ mohongOrderStatusLabel(row.status) }}</template>
</el-table-column>
<el-table-column label="下单时间" min-width="160">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="detail = row">详情</el-button>
<el-button
v-if="row.status === 'paid'"
link
type="success"
@click="handleComplete(row)"
>
完成
</el-button>
<el-button
v-if="row.status === 'pending_payment' || row.status === 'paid'"
link
type="danger"
@click="handleCancel(row)"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
<AdminTablePagination
:current-page="page"
:page-size="pageSize"
:total="total"
@update:current-page="page = $event"
@update:page-size="pageSize = $event"
@page-change="load"
/>
<el-drawer
:model-value="Boolean(detail)"
size="420px"
title="订单详情"
destroy-on-close
@update:model-value="(v: boolean) => !v && (detail = null)"
>
<template v-if="detail">
<el-descriptions :column="1" border>
<el-descriptions-item label="订单号">{{ detail.order_no }}</el-descriptions-item>
<el-descriptions-item label="状态">
{{ mohongOrderStatusLabel(detail.status) }}
</el-descriptions-item>
<el-descriptions-item label="商品">{{ detail.product_title }}</el-descriptions-item>
<el-descriptions-item label="数量">{{ detail.quantity }}</el-descriptions-item>
<el-descriptions-item label="金额">
¥{{ detail.amount || formatCent(detail.amount_cent) }}
</el-descriptions-item>
<el-descriptions-item label="买家">
{{ detail.buyer_nickname }} {{ detail.buyer_phone }}
</el-descriptions-item>
<el-descriptions-item label="会话ID">
{{ detail.conversation_id || '-' }}
</el-descriptions-item>
</el-descriptions>
<div v-if="detail.copy_text" class="copy-box">
<div class="copy-head">
<strong>复制文案</strong>
<el-button size="small" @click="copyText(detail.copy_text)">复制</el-button>
</div>
<pre>{{ detail.copy_text }}</pre>
</div>
<div v-if="detail.qrcode_url_snapshot" class="qr-box">
<strong>二维码快照</strong>
<el-image :src="detail.qrcode_url_snapshot" fit="contain" style="width: 180px; height: 180px" />
</div>
</template>
</el-drawer>
</div>
</template>
<style scoped>
.admin-page {
display: grid;
gap: 16px;
}
.page-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-head h2 {
margin: 0 0 4px;
}
.page-head p,
.sub {
margin: 0;
color: #6b7a90;
font-size: 12px;
}
.filters {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.copy-box,
.qr-box {
margin-top: 16px;
}
.copy-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.copy-box pre {
margin: 0;
padding: 12px;
background: #f7f9fc;
border-radius: 8px;
white-space: pre-wrap;
font-size: 13px;
line-height: 1.6;
}
.qr-box {
display: grid;
gap: 8px;
}
</style>
@@ -0,0 +1,418 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Upload } from '@element-plus/icons-vue'
import {
createAdminMohongProduct,
deleteAdminMohongProduct,
fetchAdminMohongCategories,
fetchAdminMohongProducts,
mohongProductStatusLabel,
updateAdminMohongProduct,
type MohongCategory,
type MohongProduct,
} from '@/features/mohong/api/mohong'
import { uploadAdminFile } from '@/shared/api/files'
import { formatCent, yuanToCent } from '@/shared/utils/money'
import { readError } from '@/shared/utils/error'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
const items = ref<MohongProduct[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const keyword = ref('')
const status = ref('')
const categoryId = ref<number | undefined>()
const categories = ref<MohongCategory[]>([])
const dialogVisible = ref(false)
const saving = ref(false)
const editingId = ref<number | null>(null)
const form = reactive({
category_id: undefined as number | undefined,
title: '',
cover_url: '',
image_urls: [] as string[],
description: '',
price_yuan: 0,
original_price_yuan: 0,
unit: '份',
stock: -1,
sort_order: 0,
status: 'draft',
qrcode_image_url: '',
use_custom_qrcode: false,
})
onMounted(async () => {
await loadCategories()
await load()
})
async function loadCategories() {
try {
categories.value = await fetchAdminMohongCategories()
} catch {
categories.value = []
}
}
async function load() {
loading.value = true
try {
const result = await fetchAdminMohongProducts({
keyword: keyword.value || undefined,
status: status.value || undefined,
category_id: categoryId.value,
page: page.value,
page_size: pageSize.value,
})
items.value = result.items || []
total.value = result.total || 0
} catch (error) {
ElMessage.error(readError(error, '加载失败'))
} finally {
loading.value = false
}
}
function openCreate() {
editingId.value = null
Object.assign(form, {
category_id: categoryId.value,
title: '',
cover_url: '',
image_urls: [],
description: '',
price_yuan: 0,
original_price_yuan: 0,
unit: '份',
stock: -1,
sort_order: 0,
status: 'on_sale',
qrcode_image_url: '',
use_custom_qrcode: false,
})
dialogVisible.value = true
}
function openEdit(item: MohongProduct) {
editingId.value = item.id
Object.assign(form, {
category_id: item.category_id || undefined,
title: item.title,
cover_url: item.cover_url,
image_urls: [...(item.image_urls || [])],
description: item.description,
price_yuan: item.price_cent / 100,
original_price_yuan: (item.original_price_cent || 0) / 100,
unit: item.unit || '份',
stock: item.stock,
sort_order: item.sort_order,
status: item.status,
qrcode_image_url: item.qrcode_image_url || '',
use_custom_qrcode: Boolean(item.qrcode_image_url),
})
dialogVisible.value = true
}
async function handleSave() {
if (!form.title.trim()) {
ElMessage.warning('请填写标题')
return
}
if (form.price_yuan <= 0) {
ElMessage.warning('请填写有效价格')
return
}
saving.value = true
try {
const payload: Record<string, unknown> = {
title: form.title.trim(),
cover_url: form.cover_url,
image_urls: form.image_urls,
description: form.description,
price_cent: yuanToCent(form.price_yuan),
original_price_cent: yuanToCent(form.original_price_yuan),
unit: form.unit || '份',
stock: form.stock,
sort_order: form.sort_order,
status: form.status,
qrcode_image_url: form.use_custom_qrcode ? form.qrcode_image_url : '',
}
if (form.category_id) {
payload.category_id = form.category_id
} else if (editingId.value) {
payload.clear_category = true
}
if (editingId.value) {
await updateAdminMohongProduct(editingId.value, payload)
ElMessage.success('已更新')
} else {
await createAdminMohongProduct(
payload as Partial<MohongProduct> & { title: string; price_cent: number }
)
ElMessage.success('已创建')
}
dialogVisible.value = false
await load()
} catch (error) {
ElMessage.error(readError(error, '保存失败'))
} finally {
saving.value = false
}
}
async function handleDelete(item: MohongProduct) {
try {
await ElMessageBox.confirm(`确认删除商品「${item.title}」?`, '提示', { type: 'warning' })
await deleteAdminMohongProduct(item.id)
ElMessage.success('已删除')
await load()
} catch (error) {
if (error !== 'cancel') ElMessage.error(readError(error, '删除失败'))
}
}
async function uploadImage(file: File, target: 'cover' | 'gallery' | 'qrcode') {
try {
const uploaded = await uploadAdminFile(file, 'mohong')
if (target === 'cover') form.cover_url = uploaded.url
else if (target === 'qrcode') form.qrcode_image_url = uploaded.url
else form.image_urls.push(uploaded.url)
ElMessage.success('上传成功')
} catch (error) {
ElMessage.error(readError(error, '上传失败'))
}
return false
}
function removeGallery(url: string) {
form.image_urls = form.image_urls.filter(item => item !== url)
}
</script>
<template>
<div class="admin-page">
<div class="page-head">
<div>
<h2>摸大红商品</h2>
<p>管理商品图片价格库存与专属二维码</p>
</div>
<div class="actions">
<el-button :icon="Refresh" @click="load">刷新</el-button>
<el-button type="primary" :icon="Plus" @click="openCreate">新建商品</el-button>
</div>
</div>
<div class="filters">
<el-input v-model="keyword" clearable placeholder="搜索标题" style="width: 220px" @keyup.enter="load" />
<el-select v-model="categoryId" clearable placeholder="分类" style="width: 140px">
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
<el-select v-model="status" clearable placeholder="状态" style="width: 140px">
<el-option label="草稿" value="draft" />
<el-option label="上架中" value="on_sale" />
<el-option label="已下架" value="off_sale" />
</el-select>
<el-button type="primary" @click=";(page = 1), load()">查询</el-button>
</div>
<el-table v-loading="loading" :data="items" border stripe>
<el-table-column label="封面" width="80">
<template #default="{ row }">
<el-image
v-if="row.cover_url"
:src="row.cover_url"
style="width: 48px; height: 48px; border-radius: 8px"
fit="cover"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="160" />
<el-table-column label="分类" width="110">
<template #default="{ row }">{{ row.category_name || '-' }}</template>
</el-table-column>
<el-table-column label="价格" width="100">
<template #default="{ row }">¥{{ row.price || formatCent(row.price_cent) }}</template>
</el-table-column>
<el-table-column label="库存" width="90">
<template #default="{ row }">{{ row.stock < 0 ? '不限' : row.stock }}</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">{{ mohongProductStatusLabel(row.status) }}</template>
</el-table-column>
<el-table-column prop="sort_order" label="排序" width="80" />
<el-table-column label="专属码" width="90">
<template #default="{ row }">{{ row.qrcode_image_url ? '是' : '默认' }}</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<AdminTablePagination
:current-page="page"
:page-size="pageSize"
:total="total"
@update:current-page="page = $event"
@update:page-size="pageSize = $event"
@page-change="load"
/>
<el-dialog
v-model="dialogVisible"
:title="editingId ? '编辑商品' : '新建商品'"
width="640px"
destroy-on-close
>
<el-form label-width="100px">
<el-form-item label="分类">
<el-select v-model="form.category_id" clearable placeholder="选择分类" style="width: 100%">
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
</el-form-item>
<el-form-item label="标题" required>
<el-input v-model="form.title" maxlength="128" />
</el-form-item>
<el-form-item label="封面">
<div class="upload-row">
<el-image
v-if="form.cover_url"
:src="form.cover_url"
style="width: 80px; height: 80px; border-radius: 8px"
fit="cover"
/>
<el-upload :show-file-list="false" accept="image/*" :before-upload="(f: File) => uploadImage(f, 'cover')">
<el-button :icon="Upload">上传封面</el-button>
</el-upload>
</div>
</el-form-item>
<el-form-item label="详情图">
<div class="gallery">
<div v-for="url in form.image_urls" :key="url" class="gallery-item">
<el-image :src="url" fit="cover" />
<button type="button" @click="removeGallery(url)">×</button>
</div>
<el-upload :show-file-list="false" accept="image/*" :before-upload="(f: File) => uploadImage(f, 'gallery')">
<el-button :icon="Upload">添加图片</el-button>
</el-upload>
</div>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="4" />
</el-form-item>
<el-form-item label="售价()" required>
<el-input-number v-model="form.price_yuan" :min="0.1" :step="0.1" :precision="1" />
</el-form-item>
<el-form-item label="原价()">
<el-input-number v-model="form.original_price_yuan" :min="0" :step="0.1" :precision="1" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="form.unit" style="width: 120px" />
</el-form-item>
<el-form-item label="库存">
<el-input-number v-model="form.stock" :min="-1" />
<span class="hint">-1 表示不限库存</span>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort_order" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="form.status" style="width: 160px">
<el-option label="草稿" value="draft" />
<el-option label="上架" value="on_sale" />
<el-option label="下架" value="off_sale" />
</el-select>
</el-form-item>
<el-form-item label="专属二维码">
<el-switch v-model="form.use_custom_qrcode" active-text="覆盖默认" inactive-text="用默认" />
<div v-if="form.use_custom_qrcode" class="upload-row" style="margin-top: 8px">
<el-image
v-if="form.qrcode_image_url"
:src="form.qrcode_image_url"
style="width: 100px; height: 100px; border-radius: 8px"
fit="contain"
/>
<el-upload
:show-file-list="false"
accept="image/*"
:before-upload="(f: File) => uploadImage(f, 'qrcode')"
>
<el-button :icon="Upload">上传二维码</el-button>
</el-upload>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.admin-page {
display: grid;
gap: 16px;
}
.page-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.page-head h2 {
margin: 0 0 4px;
}
.page-head p {
margin: 0;
color: #6b7a90;
font-size: 13px;
}
.filters {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.upload-row,
.gallery {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.gallery-item {
position: relative;
width: 72px;
height: 72px;
}
.gallery-item :deep(.el-image) {
width: 72px;
height: 72px;
border-radius: 8px;
}
.gallery-item button {
position: absolute;
top: -6px;
right: -6px;
width: 20px;
height: 20px;
border: 0;
border-radius: 50%;
background: #ef4444;
color: #fff;
cursor: pointer;
}
.hint {
margin-left: 8px;
color: #8a94a6;
font-size: 12px;
}
</style>