| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- <template>
- <div class="store-page">
- <Breadcrumb />
- <a-card :bordered="false" class="page-card" :title="isEdit ? '编辑商品' : '新增商品'">
- <a-form :model="form" :rules="rules" layout="vertical" @submit-success="handleSubmit">
- <a-row :gutter="24">
- <a-col :xs="24" :md="12">
- <a-form-item field="name" label="商品名称">
- <a-input v-model="form.name" :max-length="25" allow-clear show-word-limit />
- </a-form-item>
- </a-col>
- <a-col :xs="24" :md="12">
- <a-form-item field="state" label="上架状态">
- <a-radio-group v-model="form.state" type="button">
- <a-radio :value="1">上架</a-radio>
- <a-radio :value="0">下架</a-radio>
- </a-radio-group>
- </a-form-item>
- </a-col>
- <a-col :xs="24">
- <a-form-item field="description" label="商品简介">
- <a-textarea
- v-model="form.description"
- placeholder="展示在商城列表卡片上,建议 1~2 句话"
- :max-length="80"
- show-word-limit
- :auto-size="{ minRows: 2, maxRows: 3 }"
- />
- </a-form-item>
- </a-col>
- <a-col :xs="24" :sm="8">
- <a-form-item field="price" label="价格(元)">
- <a-input-number v-model="form.price" :step="0.01" :precision="2" :min="0" style="width: 100%" />
- </a-form-item>
- </a-col>
- <a-col :xs="24" :sm="8">
- <a-form-item field="lepao_count" label="乐跑次数">
- <a-input-number v-model="form.lepao_count" :step="1" :precision="0" :min="0" style="width: 100%" />
- </a-form-item>
- </a-col>
- <a-col :xs="24" :sm="8">
- <a-form-item field="num" label="库存">
- <a-input-number v-model="form.num" :step="1" :precision="0" :min="0" style="width: 100%" />
- </a-form-item>
- </a-col>
- <a-col :xs="24">
- <a-form-item field="icon" label="商品图标(Emoji)">
- <div class="emoji-panel">
- <div class="emoji-panel__preview">
- <span class="emoji-panel__emoji">{{ displayEmoji }}</span>
- <span class="emoji-panel__hint">当前图标</span>
- </div>
- <div class="emoji-panel__main">
- <div class="emoji-panel__label">快速选择</div>
- <div class="emoji-grid">
- <button
- v-for="emoji in presetEmojis"
- :key="emoji"
- type="button"
- class="emoji-grid__item"
- :class="{ 'emoji-grid__item--active': form.icon === emoji }"
- :title="emoji"
- @click="form.icon = emoji"
- >
- {{ emoji }}
- </button>
- </div>
- <div class="emoji-panel__label emoji-panel__label--custom">自定义</div>
- <a-input
- v-model="form.icon"
- class="emoji-custom-input"
- placeholder="输入或粘贴一个 emoji"
- :max-length="8"
- allow-clear
- />
- </div>
- </div>
- </a-form-item>
- </a-col>
- <a-col :xs="24">
- <a-form-item label="商品卖点">
- <div class="features-editor">
- <p class="features-editor__tip">展示在商城列表与详情页,最多 6 条</p>
- <div v-for="(_, index) in featureItems" :key="index" class="features-editor__row">
- <span class="features-editor__index">{{ index + 1 }}</span>
- <a-input
- v-model="featureItems[index]"
- placeholder="例如:支付后自动到账"
- :max-length="40"
- allow-clear
- />
- <a-button
- type="text"
- status="danger"
- :disabled="featureItems.length <= 1"
- @click="removeFeature(index)"
- >
- <template #icon><icon-delete /></template>
- </a-button>
- </div>
- <a-button
- type="dashed"
- long
- class="features-editor__add"
- :disabled="featureItems.length >= 6"
- @click="addFeature"
- >
- <template #icon><icon-plus /></template>
- 添加卖点
- </a-button>
- </div>
- </a-form-item>
- </a-col>
- </a-row>
- <a-form-item field="content" label="商品详情(展示在商城详情页)">
- <WangEditor v-model="form.content" @change="contentChange" />
- </a-form-item>
- <a-form-item>
- <a-space>
- <a-button type="primary" html-type="submit" :loading="loading">保存商品</a-button>
- <a-button @click="$router.push('/goodsManage/goods/goodsList')">返回列表</a-button>
- </a-space>
- </a-form-item>
- </a-form>
- </a-card>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, onMounted } from 'vue'
- import { adminGetGoods, addGoods } from '@/api/goods'
- import { useRoute } from 'vue-router'
- import { Notification } from '@arco-design/web-vue'
- import WangEditor from '@/components/Editor/WangEditor.vue'
- import {
- decodeGoodsContent,
- DEFAULT_GOODS_EMOJI,
- getGoodsEmoji,
- parseFeatures,
- serializeFeatures
- } from '@/utils/storeFormat'
- const presetEmojis = ['🏃', '⚡', '🎁', '🔥', '💪', '🏆', '📦', '⭐', '🎯', '✨']
- const loading = ref(false)
- const route = useRoute()
- const isEdit = computed(() => Boolean(route.params.id))
- const featureItems = ref([''])
- const displayEmoji = computed(() => getGoodsEmoji(form.icon))
- const rules = {
- name: [{ required: true, message: '请输入商品名称' }],
- price: [{ required: true, message: '请填写商品价格' }],
- lepao_count: [{ required: true, message: '请填写乐跑次数' }],
- num: [{ required: true, message: '请填写商品库存' }],
- state: [{ required: true, message: '请选择商品状态' }]
- }
- const form = reactive({
- name: '',
- description: '',
- price: 0.0,
- num: 999999,
- lepao_count: 0,
- ic_count: 0,
- content: '',
- state: 1,
- icon: DEFAULT_GOODS_EMOJI
- })
- const addFeature = () => {
- if (featureItems.value.length < 6) featureItems.value.push('')
- }
- const removeFeature = (index) => {
- if (featureItems.value.length <= 1) {
- featureItems.value[0] = ''
- return
- }
- featureItems.value.splice(index, 1)
- }
- const contentChange = (value) => {
- form.content = value
- }
- const handleSubmit = async () => {
- try {
- loading.value = true
- const data = {
- ...form,
- id: route.params.id ?? null,
- icon: getGoodsEmoji(form.icon),
- features: serializeFeatures(featureItems.value)
- }
- data.content = btoa(encodeURI(data.content))
- const res = await addGoods(data)
- if (!res || res.code !== 0) {
- return Notification.error({
- title: '保存商品失败!',
- content: res?.msg ?? '请稍后再试'
- })
- }
- Notification.success({ title: '保存成功!' })
- } catch (error) {
- Notification.error({
- title: '保存商品失败!',
- content: error.message || '请稍后再试'
- })
- } finally {
- loading.value = false
- }
- }
- const getGoodsDetail = async () => {
- if (!route.params.id) return
- try {
- loading.value = true
- const res = await adminGetGoods({ id: route.params.id })
- if (!res || res.code !== 0) {
- return Notification.error({
- title: '获取商品信息失败!',
- content: res?.msg ?? '请稍后再试'
- })
- }
- const { name, price, num, content, state, lepao_count, icon, description, features } = res.data
- form.content = decodeGoodsContent(content)
- form.name = name
- form.description = description || ''
- form.price = Number(price)
- form.lepao_count = lepao_count
- form.num = num
- form.state = state
- form.icon = getGoodsEmoji(icon)
- const parsed = parseFeatures(features)
- featureItems.value = parsed.length ? [...parsed] : ['']
- } catch (error) {
- Notification.error({
- title: '获取商品信息失败!',
- content: error.message || '请稍后再试'
- })
- } finally {
- loading.value = false
- }
- }
- onMounted(() => {
- getGoodsDetail()
- })
- </script>
- <style scoped lang="less">
- .page-card {
- border-radius: 12px;
- }
- .emoji-panel {
- display: flex;
- gap: 24px;
- padding: 20px;
- background: var(--color-fill-1);
- border: 1px solid var(--color-border-2);
- border-radius: 12px;
- align-items: stretch;
- @media (max-width: 640px) {
- flex-direction: column;
- }
- &__preview {
- flex-shrink: 0;
- width: 120px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 8px;
- background: var(--color-bg-2);
- border-radius: 12px;
- border: 1px solid var(--color-border-2);
- }
- &__emoji {
- font-size: 3.5rem;
- line-height: 1;
- }
- &__hint {
- font-size: 12px;
- color: var(--color-text-3);
- }
- &__main {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- &__label {
- font-size: 13px;
- font-weight: 500;
- color: var(--color-text-2);
- &--custom {
- margin-top: 4px;
- }
- }
- }
- .emoji-grid {
- display: grid;
- grid-template-columns: repeat(5, 52px);
- gap: 10px;
- @media (max-width: 480px) {
- grid-template-columns: repeat(5, 1fr);
- }
- &__item {
- width: 52px;
- height: 52px;
- padding: 0;
- font-size: 26px;
- line-height: 1;
- border: 2px solid var(--color-border-2);
- border-radius: 10px;
- background: var(--color-bg-2);
- cursor: pointer;
- transition: border-color 0.2s, background 0.2s, transform 0.15s;
- display: flex;
- align-items: center;
- justify-content: center;
- @media (max-width: 480px) {
- width: 100%;
- aspect-ratio: 1;
- height: auto;
- }
- &:hover {
- transform: scale(1.06);
- border-color: rgb(var(--primary-5));
- }
- &--active {
- border-color: rgb(var(--primary-6));
- background: var(--color-primary-light-1);
- box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.15);
- }
- }
- }
- .emoji-custom-input {
- max-width: 320px;
- height: 40px;
- }
- .features-editor {
- padding: 16px;
- background: var(--color-fill-1);
- border: 1px solid var(--color-border-2);
- border-radius: 12px;
- &__tip {
- margin: 0 0 12px;
- font-size: 12px;
- color: var(--color-text-3);
- }
- &__row {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 10px;
- }
- &__index {
- flex-shrink: 0;
- width: 22px;
- height: 22px;
- border-radius: 50%;
- background: var(--color-fill-3);
- color: var(--color-text-2);
- font-size: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- &__add {
- margin-top: 4px;
- }
- }
- </style>
|