Эх сурвалжийг харах

T12-T14: Web admin product/order/config management

- ProductManage.vue: admin product list with status filter, approve/reject/shelve actions, detail dialog
- OrderManage.vue: admin order list with status filter, detail dialog
- SysConfig.vue: admin config CRUD with inline edit dialog
- admin.js: product/order/config API functions aligned with backend paths
- router/index.js: ProductManage/OrderManage/SysConfig routes + adminRoutes list update
- Layout.vue: sidebar menu items for product-manage, order-manage, sys-config
User 5 сар өмнө
parent
commit
a45a25a9d6

+ 77 - 0
zxyj-web/src/api/admin.js

@@ -403,3 +403,80 @@ export function updateAdminInfo(data) {
     data
   })
 }
+
+// ========== 商品管理 API ==========
+
+export function getProductList(params) {
+  return request({
+    url: '/api/admin/product/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function reviewProduct(productId, data) {
+  return request({
+    url: '/api/admin/product/review',
+    method: 'post',
+    data: { productId, ...data }
+  })
+}
+
+export function shelveProduct(productId, data) {
+  return request({
+    url: '/api/product/shelve',
+    method: 'post',
+    data: { productId, ...data }
+  })
+}
+
+// ========== 订单管理 API ==========
+
+export function getOrderList(params) {
+  return request({
+    url: '/api/admin/order/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function getOrderDetail(id) {
+  return request({
+    url: `/api/product/order/detail`,
+    method: 'post',
+    data: { orderId: id }
+  })
+}
+
+// ========== 系统配置 API ==========
+
+export function getSysConfigList(params) {
+  return request({
+    url: '/api/admin/config/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function createSysConfig(data) {
+  return request({
+    url: '/api/admin/config/update',
+    method: 'post',
+    data: { configKey: data.configKey, configValue: data.configValue }
+  })
+}
+
+export function updateSysConfig(id, data) {
+  return request({
+    url: '/api/admin/config/update',
+    method: 'post',
+    data: { configKey: data.configKey, configValue: data.configValue }
+  })
+}
+
+export function deleteSysConfig(id) {
+  return request({
+    url: `/api/admin/config/delete/${id}`,
+    method: 'delete'
+  })
+}

+ 20 - 1
zxyj-web/src/router/index.js

@@ -178,6 +178,24 @@ const routes = [
         name: 'VendorReview',
         component: () => import('@/views/admin/VendorReview.vue'),
         meta: { title: '服务商审核' }
+      },
+      {
+        path: 'product-manage',
+        name: 'ProductManage',
+        component: () => import('@/views/admin/ProductManage.vue'),
+        meta: { title: '商品管理' }
+      },
+      {
+        path: 'order-manage',
+        name: 'OrderManage',
+        component: () => import('@/views/admin/OrderManage.vue'),
+        meta: { title: '订单管理' }
+      },
+      {
+        path: 'sys-config',
+        name: 'SysConfig',
+        component: () => import('@/views/admin/SysConfig.vue'),
+        meta: { title: '系统配置' }
       }
     ]
   }
@@ -266,7 +284,8 @@ router.beforeEach(async (to, from, next) => {
     'Tasks', 'TaskTemplates', 'Rewards',
     'Users', 'PackageAudit', 'GuideAudit', 'OperationLogs',
     'ServiceTypes', 'ServiceContents', 'PackageTemplates',
-    'AssessmentAdmin', 'VendorReview'
+    'AssessmentAdmin', 'VendorReview',
+    'ProductManage', 'OrderManage', 'SysConfig'
   ]
   if (adminRoutes.includes(to.name) && role === 'teacher') {
     console.warn('Access denied: Admin role required for', to.name)

+ 12 - 0
zxyj-web/src/views/Layout.vue

@@ -145,6 +145,18 @@
               <i class="el-icon-s-check"></i>
               <span slot="title">服务商审核</span>
             </el-menu-item>
+            <el-menu-item index="/product-manage">
+              <i class="el-icon-s-goods"></i>
+              <span>商品管理</span>
+            </el-menu-item>
+            <el-menu-item index="/order-manage">
+              <i class="el-icon-s-order"></i>
+              <span>订单管理</span>
+            </el-menu-item>
+            <el-menu-item index="/sys-config">
+              <i class="el-icon-s-tools"></i>
+              <span>系统配置</span>
+            </el-menu-item>
           </el-submenu>
         </template>
       </el-menu>

+ 176 - 0
zxyj-web/src/views/admin/OrderManage.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="order-manage">
+    <div class="header">
+      <h2>订单管理</h2>
+      <el-select v-model="statusFilter" placeholder="订单状态" @change="loadList" style="width: 150px;">
+        <el-option label="全部" value="" />
+        <el-option label="待支付" value="pending" />
+        <el-option label="已支付" value="paid" />
+        <el-option label="已完成" value="completed" />
+        <el-option label="已取消" value="cancelled" />
+        <el-option label="已退款" value="refunded" />
+      </el-select>
+    </div>
+
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="订单ID" width="80" />
+      <el-table-column label="商品" min-width="180">
+        <template slot-scope="{ row }">
+          <div v-if="row.productName">{{ row.productName }}</div>
+          <div v-else>-</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="买家" width="100">
+        <template slot-scope="{ row }">
+          {{ row.buyerNickname || row.nickname || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="供应商" width="100">
+        <template slot-scope="{ row }">
+          {{ row.vendorName || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="订单金额" width="100">
+        <template slot-scope="{ row }">
+          ¥{{ row.amount ? (row.amount / 100).toFixed(2) : '0.00' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="订单状态" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="支付方式" width="100">
+        <template slot-scope="{ row }">
+          {{ payMethodLabel(row.payMethod) }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="createTime" label="下单时间" width="160">
+        <template slot-scope="{ row }">
+          {{ formatTime(row.createTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button type="info" size="small" @click="viewDetail(row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      @size-change="onPageChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top: 20px; text-align: right;"
+    />
+
+    <!-- 订单详情弹窗 -->
+    <el-dialog title="订单详情" :visible.sync="detailDialogVisible" width="600px">
+      <el-descriptions :column="2" border v-if="detail">
+        <el-descriptions-item label="订单ID">{{ detail.id }}</el-descriptions-item>
+        <el-descriptions-item label="订单编号">{{ detail.orderNo || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="商品名称">{{ detail.productName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
+        <el-descriptions-item label="订单金额">¥{{ detail.amount ? (detail.amount / 100).toFixed(2) : '0.00' }}</el-descriptions-item>
+        <el-descriptions-item label="实际支付">¥{{ detail.paidAmount ? (detail.paidAmount / 100).toFixed(2) : '0.00' }}</el-descriptions-item>
+        <el-descriptions-item label="买家昵称">{{ detail.buyerNickname || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="买家手机">{{ detail.buyerPhone || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="供应商">{{ detail.vendorName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="供应商手机">{{ detail.vendorPhone || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="订单状态">
+          <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="支付方式">{{ payMethodLabel(detail.payMethod) }}</el-descriptions-item>
+        <el-descriptions-item label="支付时间" :span="2">{{ detail.paidTime ? formatTime(detail.paidTime) : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="收货地址" :span="2">{{ detail.shippingAddress || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="买家留言">{{ detail.buyerNote || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="下单时间" :span="2">{{ formatTime(detail.createTime) }}</el-descriptions-item>
+      </el-descriptions>
+      <span slot="footer">
+        <el-button @click="detailDialogVisible = false">关闭</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getOrderList } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      statusFilter: '',
+      detailDialogVisible: false,
+      detail: null
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await getOrderList({ status: this.statusFilter })
+        this.list = res.data || []
+        this.total = (res.data || []).length
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    statusType(status) {
+      const map = { pending: 'warning', paid: 'primary', completed: 'success', cancelled: 'info', refunded: 'danger' }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      const map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消', refunded: '已退款' }
+      return map[status] || status
+    },
+    payMethodLabel(method) {
+      const map = { wechat: '微信支付', balance: '余额支付', points: '积分兑换', test: '测试支付' }
+      return map[method] || method || '-'
+    },
+    typeLabel(type) {
+      const map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
+      return map[type] || type || '-'
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      return new Date(time).toLocaleString()
+    },
+    viewDetail(row) {
+      this.detail = row
+      this.detailDialogVisible = true
+    }
+  }
+}
+</script>
+
+<style scoped>
+.order-manage {
+  padding: 20px;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.header h2 {
+  margin: 0;
+}
+</style>

+ 242 - 0
zxyj-web/src/views/admin/ProductManage.vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="product-manage">
+    <div class="header">
+      <h2>商品管理</h2>
+      <div class="filters">
+        <el-select v-model="statusFilter" placeholder="商品状态" @change="loadList" style="width: 140px;">
+          <el-option label="全部" value="" />
+          <el-option label="待审核" value="pending" />
+          <el-option label="已上架" value="approved" />
+          <el-option label="已拒绝" value="rejected" />
+          <el-option label="已下架" value="shelved" />
+        </el-select>
+        <el-select v-model="typeFilter" placeholder="商品类型" @change="loadList" style="width: 140px;">
+          <el-option label="全部类型" value="" />
+          <el-option label="实物商品" value="physical" />
+          <el-option label="虚拟商品" value="virtual" />
+          <el-option label="优惠券" value="coupon" />
+        </el-select>
+      </div>
+    </div>
+
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="70" />
+      <el-table-column label="商品图片" width="80">
+        <template slot-scope="{ row }">
+          <img v-if="row.coverImage" :src="row.coverImage" class="product-thumb" @click="previewImage(row.coverImage)" />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="name" label="商品名称" min-width="150" show-overflow-tooltip />
+      <el-table-column label="类型" width="90">
+        <template slot-scope="{ row }">
+          {{ typeLabel(row.productType) }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="price" label="售价(元)" width="90">
+        <template slot-scope="{ row }">
+          ¥{{ (row.price / 100).toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="stock" label="库存" width="70" />
+      <el-table-column label="供应商" width="120" show-overflow-tooltip>
+        <template slot-scope="{ row }">
+          {{ row.vendorName || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="90">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="createTime" label="创建时间" width="160">
+        <template slot-scope="{ row }">
+          {{ formatTime(row.createTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="220" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button v-if="row.status === 'pending'" type="success" size="small" @click="handleReview(row, 'approve')">通过</el-button>
+          <el-button v-if="row.status === 'pending'" type="danger" size="small" @click="showRejectDialog(row)">拒绝</el-button>
+          <el-button v-if="row.status === 'approved'" type="warning" size="small" @click="handleShelve(row, false)">下架</el-button>
+          <el-button v-if="row.status === 'shelved'" type="primary" size="small" @click="handleShelve(row, true)">上架</el-button>
+          <el-button type="info" size="small" @click="viewDetail(row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      @size-change="onPageChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top: 20px; text-align: right;"
+    />
+
+    <!-- 拒绝原因弹窗 -->
+    <el-dialog title="拒绝原因" :visible.sync="rejectDialogVisible" width="400px">
+      <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因" />
+      <span slot="footer">
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="confirmReject">确认拒绝</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 商品详情弹窗 -->
+    <el-dialog title="商品详情" :visible.sync="detailDialogVisible" width="600px">
+      <el-descriptions :column="2" border v-if="detail">
+        <el-descriptions-item label="商品名称">{{ detail.name }}</el-descriptions-item>
+        <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
+        <el-descriptions-item label="售价">¥{{ (detail.price / 100).toFixed(2) }}</el-descriptions-item>
+        <el-descriptions-item label="会员价">¥{{ detail.memberPrice ? (detail.memberPrice / 100).toFixed(2) : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="库存">{{ detail.stock }}</el-descriptions-item>
+        <el-descriptions-item label="销量">{{ detail.salesCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="供应商">{{ detail.vendorName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="供应商手机">{{ detail.vendorPhone || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="拒绝原因" v-if="detail.rejectReason">{{ detail.rejectReason }}</el-descriptions-item>
+        <el-descriptions-item label="商品描述" :span="2">{{ detail.description || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间" :span="2">{{ formatTime(detail.createTime) }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getProductList, reviewProduct, shelveProduct } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      statusFilter: '',
+      typeFilter: '',
+      rejectDialogVisible: false,
+      rejectTarget: null,
+      rejectReason: '',
+      detailDialogVisible: false,
+      detail: null
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await getProductList({ status: this.statusFilter })
+        this.list = res.data || []
+        this.total = (res.data || []).length
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    statusType(status) {
+      const map = { pending: 'warning', approved: 'success', rejected: 'danger', shelved: 'info' }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      const map = { pending: '待审核', approved: '已上架', rejected: '已拒绝', shelved: '已下架' }
+      return map[status] || status
+    },
+    typeLabel(type) {
+      const map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
+      return map[type] || type
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      return new Date(time).toLocaleString()
+    },
+    async handleReview(row, action) {
+      try {
+        await this.$confirm(`确认通过该商品审核?`, '提示')
+        await reviewProduct(row.id, { action })
+        this.$message.success('操作成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    showRejectDialog(row) {
+      this.rejectTarget = row
+      this.rejectReason = ''
+      this.rejectDialogVisible = true
+    },
+    async confirmReject() {
+      if (!this.rejectReason.trim()) {
+        return this.$message.warning('请填写拒绝原因')
+      }
+      try {
+        await reviewProduct(this.rejectTarget.id, { action: 'reject', reason: this.rejectReason })
+        this.$message.success('已拒绝')
+        this.rejectDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
+    async handleShelve(row, shelve) {
+      try {
+        await this.$confirm(`确认${shelve ? '上架' : '下架'}该商品?`, '提示')
+        await shelveProduct(row.id, { shelve })
+        this.$message.success('操作成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    viewDetail(row) {
+      this.detail = row
+      this.detailDialogVisible = true
+    },
+    previewImage(url) {
+      if (!url) return
+      this.$alert(`<img src="${url}" style="max-width:100%;" />`, '商品图片', {
+        dangerouslyUseHTMLString: true,
+        closeOnClickModal: true
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.product-manage {
+  padding: 20px;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.header h2 {
+  margin: 0;
+}
+.filters {
+  display: flex;
+  gap: 10px;
+}
+.product-thumb {
+  width: 50px;
+  height: 50px;
+  object-fit: cover;
+  border-radius: 4px;
+  cursor: pointer;
+}
+</style>

+ 172 - 0
zxyj-web/src/views/admin/SysConfig.vue

@@ -0,0 +1,172 @@
+<template>
+  <div class="sys-config">
+    <div class="header">
+      <h2>系统配置</h2>
+      <el-button type="primary" size="small" @click="addConfig" icon="el-icon-plus">新增配置</el-button>
+    </div>
+
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="configKey" label="配置键" min-width="200" show-overflow-tooltip />
+      <el-table-column prop="configValue" label="配置值" min-width="250" show-overflow-tooltip />
+      <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
+      <el-table-column label="类型" width="90">
+        <template slot-scope="{ row }">
+          {{ typeLabel(row.valueType) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button type="primary" size="small" @click="editConfig(row)">编辑</el-button>
+          <el-button type="danger" size="small" @click="deleteConfig(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      @size-change="onPageChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top: 20px; text-align: right;"
+    />
+
+    <!-- 编辑弹窗 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" :close-on-click-modal="false">
+      <el-form :model="form" :rules="rules" ref="form" label-width="100px">
+        <el-form-item label="配置键" prop="configKey">
+          <el-input v-model="form.configKey" :disabled="!!form.id" placeholder="如: shop.welcome.banner" />
+        </el-form-item>
+        <el-form-item label="配置值" prop="configValue">
+          <el-input v-if="form.valueType === 'text'" v-model="form.configValue" type="textarea" :rows="3" placeholder="配置值" />
+          <el-input v-else v-model="form.configValue" placeholder="配置值" />
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input v-model="form.description" placeholder="配置项描述" />
+        </el-form-item>
+        <el-form-item label="值类型" prop="valueType">
+          <el-select v-model="form.valueType" style="width: 100%;">
+            <el-option label="字符串" value="string" />
+            <el-option label="数字" value="number" />
+            <el-option label="布尔值" value="boolean" />
+            <el-option label="多行文本" value="text" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmSave" :loading="submitting">保存</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getSysConfigList, createSysConfig, updateSysConfig, deleteSysConfig } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      dialogVisible: false,
+      dialogTitle: '新增配置',
+      submitting: false,
+      form: {
+        id: null,
+        configKey: '',
+        configValue: '',
+        description: '',
+        valueType: 'string'
+      },
+      rules: {
+        configKey: [{ required: true, message: '请输入配置键', trigger: 'blur' }],
+        configValue: [{ required: true, message: '请输入配置值', trigger: 'blur' }]
+      }
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await getSysConfigList({})
+        this.list = res.data || []
+        this.total = (res.data || []).length
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    typeLabel(type) {
+      const map = { string: '字符串', number: '数字', boolean: '布尔值', text: '多行文本' }
+      return map[type] || type
+    },
+    addConfig() {
+      this.form = { id: null, configKey: '', configValue: '', description: '', valueType: 'string' }
+      this.dialogTitle = '新增配置'
+      this.dialogVisible = true
+    },
+    editConfig(row) {
+      this.form = { ...row }
+      this.dialogTitle = '编辑配置'
+      this.dialogVisible = true
+    },
+    async confirmSave() {
+      const valid = await this.$refs.form.validate().catch(() => false)
+      if (!valid) return
+      this.submitting = true
+      try {
+        if (this.form.id) {
+          await updateSysConfig(this.form.id, this.form)
+        } else {
+          await createSysConfig(this.form)
+        }
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadList()
+      } catch (e) {
+        this.$message.error('保存失败')
+      } finally {
+        this.submitting = false
+      }
+    },
+    async deleteConfig(row) {
+      try {
+        await this.$confirm(`确认删除配置「${row.configKey}」?`, '警告', { type: 'warning' })
+        await deleteSysConfig(row.id)
+        this.$message.success('删除成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('删除失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.sys-config {
+  padding: 20px;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.header h2 {
+  margin: 0;
+}
+</style>