Browse Source

✨ feat: 增加商品管理

Pchen. 10 months ago
parent
commit
1c1453af13

+ 3 - 13
package.json

@@ -1,15 +1,8 @@
 {
-  "name": "gitnexus",
+  "name": "runforge",
   "private": true,
   "version": "1.0.0",
   "type": "module",
-  "author": "thc <2580797295@qq.com>",
-  "description": "",
-  "homepage": "https://www.gitnexus.cn",
-  "repository": {
-    "type": "git",
-    "url": "https://git.stackoverflow.vip/gitnexus_team/GitNexus-frontend"
-  },
   "scripts": {
     "dev": "vite",
     "build": "vite build",
@@ -17,14 +10,11 @@
   },
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
-    "@highlightjs/vue-plugin": "^2.1.0",
-    "@kjgl77/datav-vue3": "^1.7.4",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "next",
     "axios": "^1.8.3",
-    "blueimp-md5": "^2.19.0",
     "crypto-js": "^4.2.0",
     "echarts": "^5.6.0",
-    "highlight.js": "^11.11.1",
-    "html2pdf.js": "^0.10.3",
     "jsencrypt": "^3.3.2",
     "lru-cache": "^11.1.0",
     "markdown-it": "^14.1.0",

+ 27 - 1
src/api/goods.js

@@ -3,7 +3,9 @@ import request from '../utils/request'
 const api = {
   Goods: '/Goods',
   GoodsList: '/Goods/List',
-  GetCount: '/Goods/GetCount'
+  GetCount: '/Goods/GetCount',
+  AdminGoods: '/Admin/Goods',
+  AdminGoodsList: '/Admin/Goods/List'
 }
 
 export function getGoods (parameter) {
@@ -14,6 +16,22 @@ export function getGoods (parameter) {
   })
 }
 
+export function addGoods (parameter) {
+  return request({
+    url: api.AdminGoods,
+    method: 'post',
+    data: parameter
+  })
+}
+
+export function adminGetGoods (parameter) {
+  return request({
+    url: api.AdminGoods,
+    method: 'get',
+    params: parameter
+  })
+}
+
 export function getGoodsList (parameter) {
   return request({
     url: api.GoodsList,
@@ -22,6 +40,14 @@ export function getGoodsList (parameter) {
   })
 }
 
+export function adminGetGoodsList (parameter) {
+  return request({
+    url: api.AdminGoodsList,
+    method: 'get',
+    params: parameter
+  })
+}
+
 export function getCount (parameter) {
   return request({
     url: api.GetCount,

+ 71 - 0
src/components/Editor/WangEditor.vue

@@ -0,0 +1,71 @@
+<template>
+  <div style="border: 1px solid #ccc">
+    <!-- 工具栏 -->
+    <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" />
+    <!-- 编辑区 -->
+    <Editor style="height: 500px; overflow-y: hidden;" v-model="valueHtml" :defaultConfig="editorConfig" mode="default"
+      @onCreated="handleCreated" @onChange="handleChange" />
+  </div>
+</template>
+
+<script>
+import '@wangeditor/editor/dist/css/style.css'
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+
+export default {
+  name: 'WangEditorV5',
+  components: { Editor, Toolbar },
+  props: {
+    modelValue: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      editorConfig: {
+        MENU_CONF: {
+          uploadImage: {
+            server: '/cloud/api.php',
+            fieldName: 'file',
+            maxFileSize: 10 * 1024 * 1024,
+            customInsert(res, insertFn) {
+              insertFn(res.viewurl || res.downurl, res.name, res.viewurl || res.downurl)
+            }
+          },
+          uploadVideo: {
+            server: '/cloud/api.php',
+            fieldName: 'file',
+            maxFileSize: 1024 * 1024 * 1024,
+            customInsert(res, insertFn) {
+              insertFn(res.viewurl || res.downurl, res.viewurl || res.downurl)
+            }
+          }
+        }
+      }
+    }
+  },
+  watch: {
+    modelValue(val) {
+      if (val !== this.valueHtml) {
+        this.valueHtml = val
+      }
+    }
+  },
+  methods: {
+    handleCreated(editor) {
+      this.editorRef = editor
+    },
+    handleChange(editor) {
+      this.$emit('update:modelValue', this.valueHtml)
+      this.$emit('change', this.valueHtml)
+    }
+  },
+  beforeUnmount() {
+    const editor = this.editorRef
+    if (editor) {
+      editor.destroy()
+    }
+  }
+}
+</script>

+ 1 - 2
src/main.js

@@ -7,7 +7,6 @@ import App from './App.vue'
 import { router } from './router/'
 import { createPinia } from 'pinia'
 import globalComponents from '@/components'
-import DataVVue3 from '@kjgl77/datav-vue3'
 
 const app = createApp(App)
-app.use(ArcoVueIcon).use(router).use(createPinia()).use(globalComponents).use(DataVVue3).mount('#app')
+app.use(ArcoVueIcon).use(router).use(createPinia()).use(globalComponents).mount('#app')

+ 169 - 0
src/pages/admin/goods/addGoods.vue

@@ -0,0 +1,169 @@
+<template>
+    <div class="container">
+        <Breadcrumb :items="['网站管理', '添加商品']" />
+
+        <a-card title="添加商品">
+            <a-form :model="form" :rules="rules" layout="vertical" @submit-success="handleSubmit">
+                <a-form-item field="name" label="商品名称" :style="{ width: '600px' }">
+                    <a-input v-model="form.name" :max-length="25" allow-clear show-word-limit />
+                </a-form-item>
+
+                <a-form-item field="price" label="价格" :style="{ width: '300px' }">
+                    <a-input-number v-model="form.price" :step="0.01" :precision="2" />
+                </a-form-item>
+
+                <a-form-item field="lepao_count" label="乐跑次数" :style="{ width: '300px' }">
+                    <a-input-number v-model="form.lepao_count" :step="1" :precision="0" />
+                </a-form-item>
+
+                <a-form-item field="num" label="库存数量" :style="{ width: '300px' }">
+                    <a-input-number v-model="form.num" :step="1" :precision="0" />
+                </a-form-item>
+
+                <a-form-item field="state" label="商品状态" :style="{ width: '300px' }">
+                    <a-radio-group v-model="form.state">
+                        <a-radio :value="1">正常</a-radio>
+                        <a-radio :value="0">下架</a-radio>
+                    </a-radio-group>
+                </a-form-item>
+
+                <a-form-item field="content" label="商品详情">
+                    <WangEditor v-model="form.content" @change="contentChange"></WangEditor>
+                </a-form-item>
+
+                <a-form-item>
+                    <a-button html-type="submit" :loading="loading">保存</a-button>
+                </a-form-item>
+            </a-form>
+        </a-card>
+    </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { adminGetGoods, addGoods } from '@/api/goods'
+import { useRoute, useRouter } from 'vue-router'
+import { Notification } from '@arco-design/web-vue'
+import WangEditor from '@/components/Editor/WangEditor.vue'
+
+const loading = ref(false)
+const route = useRoute()
+const router = useRouter()
+
+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: '',
+    price: 0.00,
+    num: 999999,
+    files: [],
+    lepao_count: 0,
+    ic_count: 0,
+    content: '',
+    state: 1
+})
+
+const contentChange = (value) => {
+    form.content = value
+}
+
+const handleSubmit = async () => {
+    try {
+        loading.value = true
+        let data = {
+            ...form,
+            id: route.params.id ?? null
+        }
+        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: '保存成功!'
+        })
+        // router.push(`/service/orderDetail/${res.data}`)
+    } 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 } = res.data
+        form.content = decodeURI(atob(content))
+        form.name = name
+        form.price = Number(price)
+        form.num = num
+        form.state = state
+    } catch (error) {
+        Notification.error({
+            title: '获取商品信息失败!',
+            content: error.message || '请稍后再试'
+        })
+    } finally {
+        loading.value = false
+    }
+}
+
+onMounted(() => {
+    getGoodsDetail()
+})
+
+</script>
+
+<style scoped>
+.container {
+    padding: 0 20px 20px 20px;
+}
+
+.table {
+    margin-top: 15px;
+}
+</style>

+ 210 - 0
src/pages/admin/goods/goodsList.vue

@@ -0,0 +1,210 @@
+<template>
+    <div class="container">
+        <Breadcrumb :items="['网站管理', '商品管理']" />
+
+        <a-card title="商品列表">
+            <a-row>
+                <a-col :flex="1">
+                    <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
+                        label-align="left">
+                        <a-row :gutter="16">
+                            <a-col :span="8">
+                                <a-form-item field="id" label="商品名称">
+                                    <a-input-number v-model="queryData.id" placeholder="请输入商品名称关键词" :step="1"
+                                        :precision="0" />
+                                </a-form-item>
+                            </a-col>
+                        </a-row>
+                    </a-form>
+                </a-col>
+                <a-divider style="height: 84px" direction="vertical" />
+                <a-col :flex="'86px'" style="text-align: right">
+                    <a-space direction="vertical" :size="18">
+                        <a-button type="primary" @click="search">
+                            <template #icon>
+                                <icon-search />
+                            </template>
+                            搜索
+                        </a-button>
+                        <a-button @click="reset">
+                            <template #icon>
+                                <icon-refresh />
+                            </template>
+                            重置
+                        </a-button>
+                    </a-space>
+                </a-col>
+            </a-row>
+
+            <a-table :data="data" stripe hoverable column-resizable class="table" :loading="loading" :columns="columns"
+                :pagination="{
+                    showPageSize: true,
+                    showJumper: true,
+                    showTotal: true,
+                    pageSize: pagination.pagesize,
+                    current: pagination.current,
+                    total: pagination.total
+                }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
+                <template #username="{ record }">
+                    <a-avatar :size="30">
+                        <IconUser v-if="!record.avatar" />
+                        <img :alt="record.username ?? ''" :src="record.avatar" v-else />
+                    </a-avatar>
+                    {{ record.username }}
+                </template>
+                <template #create_time="{ record }">
+                    {{ stramptoTime(record.create_time) }}
+                </template>
+                <template #update_time="{ record }">
+                    {{ stramptoTime(record.update_time) }}
+                </template>
+                <template #state="{ record }">
+                    <a-tag color="orangered" v-if="record.state === 0">
+                        已下架
+                    </a-tag>
+                    <a-tag color="green" v-else-if="record.state === 1">
+                        正常
+                    </a-tag>
+                </template>
+                <template #optional="{ record }">
+                    <a-button @click="$router.push(`/admin/goods/addGoods/${record.id}`)">编辑商品</a-button>
+                </template>
+            </a-table>
+        </a-card>
+    </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { adminGetGoodsList } from '@/api/goods'
+import { Notification } from '@arco-design/web-vue'
+
+const pagination = reactive({
+    total: 0,
+    current: 1,
+    pagesize: 20
+})
+
+const queryData = reactive({
+    keyword: '',
+})
+
+const search = () => {
+    pagination.current = 1
+    getList()
+}
+
+const reset = () => {
+    pagination.current = 1
+    queryData.id = ''
+    queryData.state = -1
+    getList()
+}
+
+const columns = [
+    {
+        title: '商品ID',
+        dataIndex: 'id',
+    },
+    {
+        title: '商品名称',
+        dataIndex: 'name',
+    }, {
+        title: '商品价格',
+        dataIndex: 'price',
+    },
+    {
+        title: '库存数量',
+        dataIndex: 'num',
+    }, {
+        title: '乐跑次数',
+        dataIndex: 'lepao_count',
+    }, {
+        title: '浏览量',
+        dataIndex: 'views',
+    }, {
+        title: '创建时间',
+        slotName: 'create_time'
+    }, 
+    {
+        title: '创建人',
+        dataIndex: 'create_user',
+    },{
+        title: '最后更新时间',
+        slotName: 'update_time'
+    }, {
+        title: '更新人',
+        dataIndex: 'update_user',
+    },{
+        title: '状态',
+        slotName: 'state'
+    }, {
+        title: '操作',
+        slotName: 'optional'
+    }
+]
+
+const loading = ref(false)
+const data = ref([])
+
+const getList = async () => {
+    try {
+        loading.value = true
+
+        const reqData = {
+            ...queryData,
+            pagesize: pagination.pagesize,
+            current: pagination.current
+        }
+
+        const res = await adminGetGoodsList(reqData)
+        if (!res || res.code !== 0)
+            return Notification.error({
+                title: '获取商品失败!',
+                content: res?.msg ?? '请稍后再试'
+            })
+
+        data.value = res.data
+        console.log(res.data)
+        pagination.total = res.pagination.total
+    } catch (error) {
+        Notification.error({
+            title: '获取商品失败!',
+            content: error.message || '请稍后再试'
+        })
+    } finally {
+        loading.value = false
+    }
+}
+
+// 分页 - 页码变化
+const handlePageChange = (page) => {
+    pagination.current = page
+    getOrderList()
+}
+
+// 分页 - 每页条数变化
+const handlePageSizeChange = (size) => {
+    pagination.pagesize = size
+    pagination.current = 1 // 页大小变化后回到第一页
+    getList()
+}
+
+onMounted(() => {
+    getList()
+})
+
+const stramptoTime = (time) => {
+    return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
+}
+</script>
+
+<style scoped>
+.container {
+    padding: 0 20px 20px 20px;
+}
+
+.table {
+    margin-top: 15px;
+}
+</style>

+ 1 - 1
src/pages/service/admin/orderDetail.vue → src/pages/admin/workOrder/orderDetail.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="container">
-        <Breadcrumb :items="['工单管理', '工单详情']" />
+        <Breadcrumb :items="['网站管理', '工单管理']" />
         <a-card title="工单详情" :loading="loading">
             <a-descriptions :data="info" :column="2" />
             <div class="buttonGroup">

+ 1 - 1
src/pages/service/admin/orderList.vue → src/pages/admin/workOrder/orderList.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="container">
-        <Breadcrumb :items="['工单管理', '工单列表']" />
+        <Breadcrumb :items="['网站管理', '工单管理']" />
 
         <a-card title="工单列表">
             <a-row>

+ 69 - 38
src/router/index.js

@@ -114,14 +114,6 @@ const routes = [
             icon: 'icon-customer-service'
         },
         children: [
-            {
-                path: 'orderList',
-                name: 'service.orderList',
-                component: () => import('../pages/service/orderList.vue'),
-                meta: {
-                    title: '工单列表'
-                }
-            },
             {
                 path: 'createOrder',
                 name: 'service.createOrder',
@@ -130,6 +122,14 @@ const routes = [
                     title: '提交工单'
                 }
             },
+            {
+                path: 'orderList',
+                name: 'service.orderList',
+                component: () => import('../pages/service/orderList.vue'),
+                meta: {
+                    title: '工单列表'
+                }
+            },
             {
                 path: 'orderDetail/:id',
                 name: 'service.orderDetail',
@@ -142,50 +142,61 @@ const routes = [
         ]
     },
     {
-        path: "/user",
-        name: "user",
-        redirect: '/user/setting',
-        component: DEFAULT_LAYOUT,
-        meta: {
-            title: '个人中心',
-            icon: 'icon-user'
-        },
-        children: [
-            {
-                path: 'setting',
-                name: 'user.setings',
-                component: () => import('../pages/user/setting/index.vue'),
-                meta: {
-                    title: '用户设置'
-                },
-            }
-        ]
-    },
-        {
-        path: "/admin/service",
-        name: 'admin.service',
+        path: "/admin",
+        name: 'admin',
         redirect: '/admin/service/orderList',
         component: DEFAULT_LAYOUT,
         meta: {
-            title: '工单管理',
-            icon: 'icon-check-square'
+            title: '网站管理',
+            icon: 'icon-check-square',
+            permission: ['admin', 'service', 'product']
         },
         children: [
             {
-                path: 'orderList',
+                path: 'service/orderList',
                 name: 'admin.service.orderList',
-                component: () => import('../pages/service/admin/orderList.vue'),
+                component: () => import('../pages/admin/workOrder/orderList.vue'),
                 meta: {
-                    title: '工单列表'
+                    title: '工单管理',
+                    permission: ['admin', 'service']
                 }
             },
             {
-                path: 'orderDetail/:id',
+                path: 'service/orderDetail/:id',
                 name: 'admin.service.orderDetail',
-                component: () => import('../pages/service/admin/orderDetail.vue'),
+                component: () => import('../pages/admin/workOrder/orderDetail.vue'),
                 meta: {
                     title: '工单详情',
-                    hideInMenu: true
+                    hideInMenu: true,
+                    permission: ['admin', 'service']
+                }
+            },
+            {
+                path: 'goods/addGoods',
+                name: 'admin.goods.addGoods',
+                component: () => import('../pages/admin/goods/addGoods.vue'),
+                meta: {
+                    title: '添加商品',
+                    permission: ['admin', 'product']
+                }
+            },
+            {
+                path: 'goods/addGoods/:id',
+                name: 'admin.goods.editGoods',
+                component: () => import('../pages/admin/goods/addGoods.vue'),
+                meta: {
+                    title: '编辑商品',
+                    hideInMenu: true,
+                    permission: ['admin', 'product']
+                }
+            },
+            {
+                path: 'goods/goodsList',
+                name: 'admin.goods.goodsList',
+                component: () => import('../pages/admin/goods/goodsList.vue'),
+                meta: {
+                    title: '商品管理',
+                    permission: ['admin', 'product']
                 }
             }
         ]
@@ -220,6 +231,26 @@ const routes = [
             }
         ]
     },
+    {
+        path: "/user",
+        name: "user",
+        redirect: '/user/setting',
+        component: DEFAULT_LAYOUT,
+        meta: {
+            title: '个人中心',
+            icon: 'icon-user'
+        },
+        children: [
+            {
+                path: 'setting',
+                name: 'user.setings',
+                component: () => import('../pages/user/setting/index.vue'),
+                meta: {
+                    title: '用户设置'
+                },
+            }
+        ]
+    }
 ]
 
 const router = VueRouter.createRouter({

+ 1 - 1
vite.config.js

@@ -18,7 +18,7 @@ export default defineConfig({
   server: {
     proxy: {
       '/cloud': {
-        target: 'http://lepao-cloud.xxoo365.top/',
+        target: 'https://xxoo365.top/cloud',
         changeOrigin: true,
         rewrite: path => path.replace(/^\/cloud/, '')
       }