3 Komitmen b9ef70e013 ... 198c45976f

Pembuat SHA1 Pesan Tanggal
  ylong 198c45976f feat: 新增统计分析模块的图表和组件 4 hari lalu
  ylong 584e1156a2 feat(mallOrder): 新增订单管理相关组件和页面 4 hari lalu
  ylong ba5ed0822b feat: 新增商城物流地址管理和运费模板功能 4 hari lalu
38 mengubah file dengan 5246 tambahan dan 260 penghapusan
  1. 56 0
      src/components/Chart/BaseChart.vue
  2. 79 0
      src/components/Chart/FunnelChart.vue
  3. 106 0
      src/views/mallLogistics/address/components/edit-dialog.vue
  4. 21 0
      src/views/mallLogistics/address/components/page-search.vue
  5. 91 0
      src/views/mallLogistics/address/index.vue
  6. 226 0
      src/views/mallLogistics/feeTemplate/components/area-selector.vue
  7. 166 0
      src/views/mallLogistics/feeTemplate/components/edit-dialog.vue
  8. 128 0
      src/views/mallLogistics/feeTemplate/index.vue
  9. 68 0
      src/views/mallOrder/all/components/add-package-dialog.vue
  10. 200 0
      src/views/mallOrder/all/components/order-base-info.vue
  11. 202 0
      src/views/mallOrder/all/components/order-detail.vue
  12. 288 0
      src/views/mallOrder/all/components/order-item.vue
  13. 202 0
      src/views/mallOrder/all/components/order-product-list.vue
  14. 74 0
      src/views/mallOrder/all/components/order-table-header.vue
  15. 116 0
      src/views/mallOrder/all/components/page-search.vue
  16. 209 0
      src/views/mallOrder/all/components/push-sms-dialog.vue
  17. 234 0
      src/views/mallOrder/all/components/refund-dialog.vue
  18. 198 0
      src/views/mallOrder/all/index.vue
  19. 46 0
      src/views/mallOrder/complaint/components/complain-item.vue
  20. 149 0
      src/views/mallOrder/complaint/components/negotiate-dialog.vue
  21. 381 0
      src/views/mallOrder/complaint/components/page-edit.vue
  22. 69 0
      src/views/mallOrder/complaint/components/page-search.vue
  23. 125 0
      src/views/mallOrder/complaint/index.vue
  24. 0 0
      src/views/mallOrder/refund/index.vue
  25. 207 260
      src/views/recycleLogistics/warehouse/components/area-setting.vue
  26. 172 0
      src/views/statsAnalysis/marketing/components/ActivityAnalysis.vue
  27. 218 0
      src/views/statsAnalysis/marketing/components/MarketingOverview.vue
  28. 132 0
      src/views/statsAnalysis/marketing/index.vue
  29. 140 0
      src/views/statsAnalysis/search/components/SearchEffectModal.vue
  30. 142 0
      src/views/statsAnalysis/search/components/SearchOverview.vue
  31. 35 0
      src/views/statsAnalysis/search/components/SearchTable.vue
  32. 70 0
      src/views/statsAnalysis/search/components/SearchTrendModal.vue
  33. 92 0
      src/views/statsAnalysis/search/index.vue
  34. 106 0
      src/views/statsAnalysis/sellData/components/DistributionCharts.vue
  35. 110 0
      src/views/statsAnalysis/sellData/components/OrderTrendChart.vue
  36. 157 0
      src/views/statsAnalysis/sellData/components/RefundStats.vue
  37. 111 0
      src/views/statsAnalysis/sellData/components/SalesStatCards.vue
  38. 120 0
      src/views/statsAnalysis/sellData/index.vue

+ 56 - 0
src/components/Chart/BaseChart.vue

@@ -0,0 +1,56 @@
+<template>
+  <v-chart ref="chartRef" :option="option" :autoresize="true" class="base-chart" />
+</template>
+
+<script setup>
+import { ref, toRefs } from 'vue';
+import VChart from 'vue-echarts';
+import { useEcharts } from '@/utils/use-echarts';
+import { use } from 'echarts/core';
+import { CanvasRenderer } from 'echarts/renderers';
+import { BarChart, PieChart, LineChart } from 'echarts/charts';
+import {
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  DataZoomComponent,
+  GraphicComponent
+} from 'echarts/components';
+
+use([
+  CanvasRenderer,
+  BarChart,
+  PieChart,
+  LineChart,
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  DataZoomComponent,
+  GraphicComponent
+]);
+
+const props = defineProps({
+  option: {
+    type: Object,
+    required: true,
+    default: () => ({})
+  }
+});
+
+const { option } = toRefs(props);
+const chartRef = ref(null);
+
+useEcharts([chartRef]);
+
+defineExpose({
+  chartRef
+});
+</script>
+
+<style scoped>
+.base-chart {
+  width: 100%;
+  height: 100%;
+  min-height: 300px;
+}
+</style>

+ 79 - 0
src/components/Chart/FunnelChart.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="funnel-chart-container">
+    <svg viewBox="0 0 800 320" preserveAspectRatio="xMidYMid meet" class="funnel-svg">
+      <defs>
+        <filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
+          <feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.1" />
+        </filter>
+      </defs>
+
+      <!-- Conversion Line (Moved to background) -->
+      <polyline points="630,105 700,105 650,210 500,210" fill="none" stroke="#999" stroke-width="2" />
+      <text x="710" y="150" fill="#666" font-size="20">转化率</text>
+      <text x="710" y="180" fill="#333" font-size="24" font-weight="bold">{{ conversionRate }}</text>
+
+      <!-- Group 1: Visitors (Top) -->
+      <g transform="translate(0, 0)">
+        <!-- Left: Light Blue -->
+        <!-- Shape: Rect left, slanted right edge -->
+        <path d="M50,10 L350,10 L380,150 L50,150 Z" fill="#9ac1d9" stroke="none" />
+        
+        <!-- Right: Dark Blue -->
+        <!-- Shape: Slanted left edge (parallel gap), slanted right edge -->
+        <path d="M385,10 L700,10 L650,150 L415,150 Z" fill="#4a8aff" stroke="none" />
+        
+        <!-- Text: Left -->
+        <text x="150" y="60" fill="#fff" font-size="20">领取人数</text>
+        <text x="150" y="100" fill="#fff" font-size="24" font-weight="bold">{{ visitorCount }}</text>
+
+        <!-- Text: Right -->
+        <text x="540" y="90" text-anchor="middle" fill="#fff" font-size="32" font-weight="bold">{{ visitorLabel }}</text>
+      </g>
+
+      <!-- Group 2: Payment (Bottom) -->
+      <g transform="translate(0, 155)">
+        <!-- Left: Light Teal -->
+        <path d="M50,0 L380,0 L530,150 L50,150 Z" fill="#a5d1d3" stroke="none" />
+        
+        <!-- Right: Dark Teal (Triangle) -->
+        <path d="M415,0 L650,0 L532.5,150 Z" fill="#63c2cc" stroke="none" />
+
+        <!-- Text: Left -->
+        <!-- 营销支付人数 -->
+        <text x="100" y="60" fill="#fff" font-size="18">营销支付人数</text>
+        <text x="150" y="100" fill="#fff" font-size="22" font-weight="bold">{{ paymentCount }}</text>
+
+        <!-- 客单价 -->
+        <text x="280" y="60" fill="#fff" font-size="18">客单价</text>
+        <text x="310" y="100" fill="#fff" font-size="22" font-weight="bold">{{ unitPrice }}</text>
+
+        <!-- Text: Right -->
+        <text x="532" y="60" text-anchor="middle" fill="#fff" font-size="32" font-weight="bold">{{ paymentLabel }}</text>
+      </g>
+
+    </svg>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  visitorLabel: { type: String, default: '访客' },
+  paymentLabel: { type: String, default: '支付' },
+  visitorCount: { type: [Number, String], default: 0 },
+  paymentCount: { type: [Number, String], default: 0 },
+  unitPrice: { type: [Number, String], default: 0 },
+  conversionRate: { type: String, default: '0%' }
+});
+</script>
+
+<style scoped>
+.funnel-chart-container {
+  width: 100%;
+  height: 100%;
+  min-height: 250px;
+}
+.funnel-svg {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 106 - 0
src/views/mallLogistics/address/components/edit-dialog.vue

@@ -0,0 +1,106 @@
+<template>
+    <simple-form-modal :title="title" :items="formItems" ref="editRef" :baseUrl="baseUrl"
+        @success="(data) => emit('success', data)" :formatData="formatData">
+        <template #areaSelect="{ model }">
+            <area-select v-model="model.proCodes" />
+        </template>
+    </simple-form-modal>
+</template>
+
+<script setup>
+import { reactive, ref, defineEmits, computed, nextTick } from 'vue';
+import SimpleFormModal from '@/components/CommonPage/SimpleFormModal.vue';
+import AreaSelect from '@/components/CommonPage/AreaSelect.vue';
+
+const emit = defineEmits(['success']);
+
+const title = ref('添加地址');
+
+const formItems = computed(() => {
+    return [
+        { type: 'input', label: '联系人', prop: 'contactName', required: true },
+        {
+            type: 'areaSelect',
+            label: '所在地区',
+            prop: 'proCodes',
+            required: true
+        },
+        {
+            type: 'textarea',
+            label: '详细地址',
+            prop: 'detailAddress',
+            required: true
+        },
+        {
+            type: 'input',
+            label: '手机号',
+            prop: 'phone',
+            itemProps: {
+                rules: [
+                    { required: true, message: '请输入手机号', trigger: ['blur'] },
+                    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: ['change'] }
+                ]
+            }
+        },
+        { type: 'input', label: '邮编编码', prop: 'zipCode' },
+        { type: 'textarea', label: '备注', prop: 'remark' }
+    ];
+});
+
+const baseUrl = reactive({
+    add: '/mallLogistics/address/add',
+    edit: '/mallLogistics/address/update'
+});
+
+const formData = ref({
+    id: undefined,
+    contactName: '',
+    phone: '',
+    proCodes: [],
+    detailAddress: '',
+    zipCode: '',
+    remark: ''
+});
+
+const editRef = ref(null);
+
+const formatData = (data) => {
+    if (data.proCodes && data.proCodes.length > 0) {
+        data.provinceId = data.proCodes[0];
+        data.cityId = data.proCodes[1];
+        data.districtId = data.proCodes[2];
+    }
+    return data;
+};
+
+const handleOpen = (row) => {
+    formData.value = {
+        id: undefined,
+        contactName: '',
+        phone: '',
+        proCodes: [],
+        detailAddress: '',
+        zipCode: '',
+        remark: ''
+    };
+
+    if (row && row.id) {
+        title.value = '编辑地址';
+        formData.value = { ...row };
+        // Map province/city/district to proCodes if they exist separately in row
+        if (row.provinceId && row.cityId && row.districtId) {
+            formData.value.proCodes = [row.provinceId, row.cityId, row.districtId];
+        } else if (row.proCodes) {
+            formData.value.proCodes = row.proCodes;
+        }
+    } else {
+        title.value = '添加地址';
+    }
+
+    nextTick(() => {
+        editRef.value?.handleOpen(formData.value);
+    });
+};
+
+defineExpose({ handleOpen });
+</script>

+ 21 - 0
src/views/mallLogistics/address/components/page-search.vue

@@ -0,0 +1,21 @@
+<template>
+	<ele-card :body-style="{ paddingBottom: '8px' }">
+		<ProSearch ref="proSearchRef" :items="formItems" @search="search" />
+	</ele-card>
+</template>
+
+<script setup>
+import { reactive, defineEmits } from 'vue';
+import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+const emit = defineEmits(['search']);
+
+const formItems = reactive([
+	{ type: 'input', label: '联系人', prop: 'contactName', placeholder: '请输入联系人' },
+	{ type: 'input', label: '手机号码', prop: 'phone', placeholder: '请输入手机号码' },
+]);
+
+const search = (data) => {
+	emit('search', data);
+};
+</script>

+ 91 - 0
src/views/mallLogistics/address/index.vue

@@ -0,0 +1,91 @@
+<template>
+    <ele-page flex-table>
+        <PageSearch @search="handleSearch" />
+
+        <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns">
+            <template #toolbar>
+                <div class="flex items-center space-x-2">
+                    <el-button type="primary" @click="handleAdd" :icon="Plus">添加新地址</el-button>
+                </div>
+            </template>
+
+            <!-- Custom Slots for Radio Buttons -->
+            <template #isSend="{ row }">
+                <el-radio :model-value="row.isSend" :label="1" @change="handleSetDefault(row, 'send')">默认</el-radio>
+            </template>
+
+            <template #isReturn="{ row }">
+                <el-radio :model-value="row.isReturn" :label="1" @change="handleSetDefault(row, 'return')">默认</el-radio>
+            </template>
+
+            <template #action="{ row }">
+                <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+                <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
+            </template>
+        </common-table>
+
+        <EditDialog ref="editDialogRef" @success="refreshPage" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive, getCurrentInstance } from 'vue';
+import { Plus } from '@element-plus/icons-vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import PageSearch from './components/page-search.vue';
+import EditDialog from './components/edit-dialog.vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+const { proxy } = getCurrentInstance();
+
+const tableRef = ref(null);
+const editDialogRef = ref(null);
+
+const pageConfig = reactive({
+    pageUrl: '/mallLogistics/address/page',
+    rowKey: 'id',
+    fileName: '地址管理',
+    params: {}
+});
+
+const columns = ref([
+    { prop: 'isSend', label: '发货地址', slot: 'isSend', width: 100, align: 'center' },
+    { prop: 'isReturn', label: '退货地址', slot: 'isReturn', width: 100, align: 'center' },
+    { prop: 'contactName', label: '联系人', minWidth: 100 },
+    { prop: 'area', label: '所在地区', minWidth: 150 },
+    { prop: 'detailAddress', label: '详细地址', minWidth: 200 },
+    { prop: 'phone', label: '手机号码', width: 120 },
+    { columnKey: 'action', label: '操作', width: 150, fixed: 'right', align: 'center', slot: 'action' }
+]);
+
+const handleSearch = (params) => {
+    tableRef.value?.reload({ where: params });
+};
+
+const handleAdd = () => {
+    editDialogRef.value?.handleOpen();
+};
+
+const handleEdit = (row) => {
+    editDialogRef.value?.handleOpen(row);
+};
+
+const handleDelete = (row) => {
+    tableRef.value.messageConfrim({
+        message: "确认删除该地址?",
+        fetch: () => proxy.$http.post('/mallLogistics/address/delete', { id: row.id })
+    })
+};
+
+const handleSetDefault = (row, type) => {
+    const url = type === 'send' ? '/mallLogistics/address/setDefaultSend' : '/mallLogistics/address/setDefaultReturn';
+    proxy.$http.post(url, { id: row.id }).then(() => {
+        ElMessage.success('设置成功');
+        refreshPage();
+    });
+};
+
+const refreshPage = () => {
+    tableRef.value?.reload();
+};
+</script>

+ 226 - 0
src/views/mallLogistics/feeTemplate/components/area-selector.vue

@@ -0,0 +1,226 @@
+<!-- 区域选择弹窗 -->
+<template>
+    <ele-modal form :width="1080" :bodyStyle="{ 'min-height': '400px' }" v-model="visible" :title="title"
+        @open="handleOpen">
+        <div class="flex flex-col" v-loading="loading">
+            <div class="flex" v-for="dq in areaList" :key="dq.code">
+                <el-checkbox :label="dq.name" :indeterminate="isIndeterminateShow(dq)" style="min-width: 100px"
+                    @change="(value) => handleDqChange(value, dq)" />
+
+                <div v-for="item in dq.children" class="mr-6 flex items-center" :key="item.code">
+                    <!-- Province Checkbox -->
+                    <el-checkbox @change="(value) => handleParentChange(value, item)" :data-id="item.id"
+                        v-model="item.mySelected" style="margin-right: 5px" :true-value="1" :false-value="0"
+                        :indeterminate="isIndeterCityShow(item, 'mySelected')" />
+                    
+                    <ele-popover :width="400" trigger="click" :title="item.district" :showArrow="false"
+                        :disabled="isPopoverDisabled(item)">
+                        <template #reference>
+                            <div class="flex items-center cursor-pointer" :class="{
+                                'disabled-popover': isPopoverDisabled(item)
+                            }">
+                                <el-text :type="item.mySelected == 1 ? 'primary' : ''
+                                    ">{{ item.district }}</el-text>
+                                <el-text type="warning" v-if="isShowAll(item)">(全)</el-text>
+                                <el-icon>
+                                    <CaretBottom />
+                                </el-icon>
+                            </div>
+                        </template>
+                        <template v-for="child in item.childInfo" :key="child.id">
+                            <el-checkbox @change="
+                                (value) =>
+                                    handleChildChange(value, item, child)
+                            " :data-id="child.id" v-model="child.mySelected" :label="child.district" :true-value="1"
+                                :false-value="0" />
+                        </template>
+                    </ele-popover>
+                </div>
+            </div>
+        </div>
+
+        <template #footer>
+            <el-button @click="handleCancel">取消</el-button>
+            <el-button type="primary" @click="handleSubmit">确定</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, nextTick, computed, defineEmits, defineExpose } from 'vue';
+import { CaretBottom } from '@element-plus/icons-vue';
+
+const emit = defineEmits(['confirm']);
+const visible = defineModel({ type: Boolean });
+
+const title = ref('选择配送区域');
+const loading = ref(false);
+const areaList = ref([]);
+
+// Mock Data Generation
+const generateMockData = () => {
+    const regions = {
+        'hd': ['上海', '江苏', '浙江', '安徽', '福建', '江西', '山东'],
+        'hb': ['北京', '天津', '河北', '山西', '内蒙古'],
+        'hz': ['河南', '湖北', '湖南'],
+        'hn': ['广东', '广西', '海南'],
+        'db': ['辽宁', '吉林', '黑龙江'],
+        'xb': ['陕西', '甘肃', '青海', '宁夏', '新疆'],
+        'xn': ['重庆', '四川', '贵州', '云南', '西藏'],
+        'gat': ['香港', '澳门', '台湾'],
+        'hw': ['海外']
+    };
+    
+    const mockData = {};
+    let idCounter = 1;
+
+    for (const [code, provinces] of Object.entries(regions)) {
+        mockData[code] = provinces.map(prov => ({
+            id: idCounter++,
+            district: prov,
+            code: code, // Adding code for key if needed
+            mySelected: 0,
+            otherSelected: 0,
+            childInfo: [] // For simplicity, no cities for now, or maybe just one '全境' city if needed, but let's assume province level for fee template mostly. 
+                          // Wait, the original code uses childInfo for popover. Let's add dummy cities.
+        }));
+        
+        // Add dummy cities for interaction testing
+        mockData[code].forEach(prov => {
+             prov.childInfo = [
+                 { id: idCounter++, district: prov.district + '市1', mySelected: 0, otherSelected: 0 },
+                 { id: idCounter++, district: prov.district + '市2', mySelected: 0, otherSelected: 0 }
+             ];
+        });
+    }
+    return mockData;
+};
+
+// Define Regions
+const dqList = ref([
+    { name: '华东', code: 'hd' },
+    { name: '华北', code: 'hb' },
+    { name: '华中', code: 'hz' },
+    { name: '华南', code: 'hn' },
+    { name: '东北', code: 'db' },
+    { name: '西北', code: 'xb' },
+    { name: '西南', code: 'xn' },
+    { name: '港澳台', code: 'gat' },
+    { name: '海外', code: 'hw' }
+]);
+
+const handleOpen = (existingSelection) => {
+    visible.value = true;
+    loading.value = true;
+    
+    // Simulate API call
+    setTimeout(() => {
+        const data = generateMockData();
+        areaList.value = [];
+        dqList.value.forEach((item) => {
+            const dqItem = { ...item, checked: false, children: data[item.code] || [] };
+            areaList.value.push(dqItem);
+        });
+
+        // Restore selection if passed (simplified restoration logic)
+        // If existingSelection is passed, we would need to map it back to mySelected=1
+        if (existingSelection && Array.isArray(existingSelection)) {
+             // Logic to restore selection would go here
+             // For now, let's just clear
+        }
+        
+        loading.value = false;
+    }, 300);
+};
+
+const handleCancel = () => {
+    visible.value = false;
+};
+
+// Computed Properties
+const isPopoverDisabled = computed(() => (item) => {
+    // Simplified: disabled if all children are 'otherSelected' (not applicable in this standalone version usually)
+    return item.childInfo.length === 0;
+});
+
+const isShowAll = computed(() => (item) => {
+    return item.childInfo.length > 0 && item.childInfo.every((i) => i.mySelected == 1);
+});
+
+const isIndeterminateShow = computed(() => (dq) => {
+    let len = dq.children.length;
+    let length = dq.children.filter((i) => i.mySelected == 1).length;
+    return length > 0 && length < len;
+});
+
+const isIndeterCityShow = computed(() => (item, type) => {
+    let len = item.childInfo.length;
+    let length = item.childInfo.filter((i) => i[type] == 1).length;
+    return length > 0 && length < len;
+});
+
+// Event Handlers
+const handleDqChange = (value, dq) => {
+    const val = value ? 1 : 0;
+    dq.children.forEach((item) => {
+        item.mySelected = val;
+        item.childInfo.forEach((i) => {
+            i.mySelected = val;
+        });
+    });
+};
+
+const handleParentChange = (value, item) => {
+    const val = value ? 1 : 0;
+    item.childInfo.forEach((i) => {
+        i.mySelected = val;
+    });
+};
+
+const handleChildChange = (value, item, child) => {
+    let checked = item.childInfo.some((i) => i.mySelected == 1);
+    item.mySelected = checked ? 1 : 0;
+};
+
+const handleSubmit = () => {
+    // Collect selected regions
+    const selected = [];
+    areaList.value.forEach(dq => {
+        dq.children.forEach(prov => {
+            if (prov.mySelected === 1) {
+                // If all cities selected, just push province name
+                if (isShowAll.value(prov)) {
+                     selected.push(prov.district);
+                } else {
+                    // Else push selected cities
+                    const cities = prov.childInfo.filter(c => c.mySelected === 1).map(c => c.district);
+                    if (cities.length > 0) {
+                        selected.push(`${prov.district}(${cities.join(',')})`);
+                    }
+                }
+            } else {
+                 // Check partial selection
+                 const cities = prov.childInfo.filter(c => c.mySelected === 1).map(c => c.district);
+                 if (cities.length > 0) {
+                      selected.push(`${prov.district}(${cities.join(',')})`);
+                 }
+            }
+        });
+    });
+    
+    emit('confirm', selected);
+    visible.value = false;
+};
+
+defineExpose({
+    handleOpen
+});
+</script>
+
+<style lang="scss" scoped>
+.disabled-popover {
+    cursor: not-allowed;
+    .el-text { color: #909399; }
+    .el-icon { color: #909399; fill: #909399; }
+}
+</style>

+ 166 - 0
src/views/mallLogistics/feeTemplate/components/edit-dialog.vue

@@ -0,0 +1,166 @@
+<template>
+    <ele-modal
+        :width="1000"
+        v-model="visible"
+        :title="title"
+        @open="handleOpen"
+        :body-style="{ paddingBottom: '20px' }"
+    >
+        <el-form :model="form" ref="formRef" :rules="rules" label-width="100px">
+            <el-form-item label="模板名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入模板名称" />
+            </el-form-item>
+            <el-form-item label="配送方式" prop="deliveryMethod">
+                <el-radio-group v-model="form.deliveryMethod">
+                    <el-radio :label="1">快递</el-radio>
+                </el-radio-group>
+            </el-form-item>
+
+            <!-- Table for Rules -->
+            <div class="px-4">
+                <el-table :data="form.rules" border style="width: 100%">
+                    <el-table-column label="选择地区" min-width="250">
+                        <template #default="{ row, $index }">
+                            <div class="flex items-center justify-between">
+                                <span v-if="!row.regions || row.regions.length === 0" class="text-gray-400">未选择地区</span>
+                                <span v-else class="truncate pr-2" :title="row.regions.join(',')">{{ row.regions.join('、') }}</span>
+                                <el-button type="primary" link @click="openAreaSelector($index)">编辑</el-button>
+                            </div>
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="设置包邮条件" width="220">
+                         <template #default="{ row }">
+                            <div class="flex items-center">
+                                <span class="mr-2">满</span>
+                                <el-input-number v-model="row.freeAmount" :min="0" :precision="2" :controls="false" style="width: 80px" />
+                                <span class="ml-2">元包邮</span>
+                            </div>
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="基础运费" width="150">
+                        <template #default="{ row }">
+                            <el-input-number v-model="row.baseFee" :min="0" :precision="2" :controls="false" style="width: 100%" />
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="操作" width="80" align="center">
+                        <template #default="{ $index }">
+                            <el-button type="danger" link @click="removeRule($index)" :disabled="$index === 0 && form.rules.length === 1">
+                                <el-icon><Delete /></el-icon>
+                            </el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+                <div class="mt-2">
+                    <el-button type="primary" link :icon="Plus" @click="addRule">添加规则</el-button>
+                </div>
+            </div>
+            
+            <!-- Default Rule Tip or separate section? Figure 2 shows rows being added. -->
+        </el-form>
+
+        <template #footer>
+            <el-button @click="visible = false">取消</el-button>
+            <el-button type="primary" @click="submit">确定</el-button>
+        </template>
+
+        <AreaSelector v-model="areaSelectorVisible" ref="areaSelectorRef" @confirm="handleAreaConfirm" />
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, nextTick, defineEmits, defineExpose } from 'vue';
+import { Delete, Plus } from '@element-plus/icons-vue';
+import { ElMessage } from 'element-plus';
+import AreaSelector from './area-selector.vue';
+
+const visible = defineModel({ type: Boolean });
+const emit = defineEmits(['success']);
+
+const title = ref('新增运费模板');
+const formRef = ref(null);
+const areaSelectorRef = ref(null);
+const areaSelectorVisible = ref(false);
+const currentRuleIndex = ref(-1);
+
+const form = reactive({
+    id: undefined,
+    name: '',
+    deliveryMethod: 1,
+    rules: [
+        { regions: ['全国'], freeAmount: 0, baseFee: 0 } // Default rule
+    ]
+});
+
+const rules = {
+    name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
+    deliveryMethod: [{ required: true, message: '请选择配送方式', trigger: 'change' }]
+};
+
+const handleOpen = (row) => {
+    visible.value = true;
+    nextTick(() => {
+        formRef.value?.resetFields();
+        if (row && row.id) {
+            title.value = '编辑运费模板';
+            Object.assign(form, JSON.parse(JSON.stringify(row)));
+        } else {
+            title.value = '新增运费模板';
+            form.id = undefined;
+            form.name = '';
+            form.deliveryMethod = 1;
+            form.rules = [{ regions: [], freeAmount: undefined, baseFee: 5 }];
+        }
+    });
+};
+
+const addRule = () => {
+    form.rules.push({ regions: [], freeAmount: undefined, baseFee: 5 });
+};
+
+const removeRule = (index) => {
+    form.rules.splice(index, 1);
+};
+
+const openAreaSelector = (index) => {
+    currentRuleIndex.value = index;
+    areaSelectorVisible.value = true;
+    nextTick(() => {
+        areaSelectorRef.value?.handleOpen(form.rules[index].regions);
+    });
+};
+
+const handleAreaConfirm = (selectedRegions) => {
+    if (currentRuleIndex.value !== -1) {
+        form.rules[currentRuleIndex.value].regions = selectedRegions;
+    }
+};
+
+const submit = () => {
+    formRef.value.validate((valid) => {
+        if (valid) {
+            // Validate rules
+            for (let i = 0; i < form.rules.length; i++) {
+                const rule = form.rules[i];
+                if (!rule.regions || rule.regions.length === 0) {
+                    ElMessage.warning(`第 ${i + 1} 行未选择地区`);
+                    return;
+                }
+                if (rule.baseFee === undefined || rule.baseFee === null) {
+                     ElMessage.warning(`第 ${i + 1} 行未设置基础运费`);
+                     return;
+                }
+            }
+            
+            // Mock submission
+            console.log('Submitting:', form);
+            ElMessage.success('保存成功');
+            emit('success', { ...form, id: form.id || Date.now() }); // Simulate returning saved data
+            visible.value = false;
+        }
+    });
+};
+
+defineExpose({
+    handleOpen
+});
+</script>

+ 128 - 0
src/views/mallLogistics/feeTemplate/index.vue

@@ -0,0 +1,128 @@
+<template>
+    <ele-page>
+        <div class="bg-white p-4">
+            <div class="mb-4">
+                <el-button type="success" :icon="Plus" @click="handleAdd">添加运费模板</el-button>
+            </div>
+
+            <div v-if="templateList.length === 0" class="text-center py-10 text-gray-400">
+                暂无运费模板
+            </div>
+
+            <div v-for="(tpl, index) in templateList" :key="tpl.id"
+                class="mb-6 border border-gray-200 rounded-lg shadow-sm overflow-hidden bg-white">
+                <!-- Template Header -->
+                <div class="bg-green-50 px-4 py-3 flex justify-between items-center border-b border-gray-200">
+                    <span class="font-bold text-gray-700">{{ tpl.name }}</span>
+                    <div class="space-x-4 text-sm">
+                        <el-button type="primary" link @click="handleCopy(tpl)">复制模板</el-button>
+                        <el-button type="primary" link @click="handleEdit(tpl)">编辑</el-button>
+                        <el-button type="danger" link @click="handleDelete(index)">删除</el-button>
+                    </div>
+                </div>
+
+                <!-- Rules Table -->
+                <el-table :data="tpl.rules" style="width: 100%" :show-header="true">
+                    <el-table-column prop="regions" label="运送范围" min-width="300">
+                        <template #default="{ row }">
+                            <span v-if="!row.regions || row.regions.length === 0">未指定地区</span>
+                            <span v-else>{{ row.regions.join('、') }}</span>
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="计费规则" width="200" align="center">
+                        <template #default="{ row }">
+                            <span v-if="row.freeAmount > 0">满{{ row.freeAmount }}元包邮</span>
+                            <span v-else>不支持配送/默认</span>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="baseFee" label="基础运费" width="150" align="center" />
+                    <el-table-column label="配送方式" width="150" align="center">
+                        <template #default>
+                            {{ tpl.deliveryMethod === 1 ? '快递' : '其他' }}
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+
+            <EditDialog ref="editDialogRef" @success="handleSaveSuccess" />
+        </div>
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { Plus } from '@element-plus/icons-vue';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import EditDialog from './components/edit-dialog.vue';
+
+const editDialogRef = ref(null);
+
+// Mock Data
+const templateList = ref([
+    {
+        id: 1,
+        name: '运费模板名称',
+        deliveryMethod: 1,
+        rules: [
+            {
+                regions: ['江苏', '浙江', '安徽', '江西', '北京', '天津', '山西', '山东', '河北', '湖南', '湖北', '河南', '广东', '广西', '福建', '辽宁', '吉林', '黑龙江', '陕西', '甘肃', '宁夏', '重庆', '云南', '贵州', '四川'],
+                freeAmount: 5.8,
+                baseFee: 5
+            },
+            {
+                regions: ['新疆', '西藏', '香港', '澳门', '台湾', '海外'],
+                freeAmount: 0, // Assuming 0 means not supported or default fee
+                baseFee: 5
+            },
+            {
+                regions: ['上海', '内蒙古', '青海'],
+                freeAmount: 6.8,
+                baseFee: 5
+            },
+            {
+                regions: ['海南'],
+                freeAmount: 9.8,
+                baseFee: 5
+            }
+        ]
+    }
+]);
+
+const handleAdd = () => {
+    editDialogRef.value?.handleOpen();
+};
+
+const handleEdit = (tpl) => {
+    editDialogRef.value?.handleOpen(tpl);
+};
+
+const handleCopy = (tpl) => {
+    const newTpl = JSON.parse(JSON.stringify(tpl));
+    newTpl.id = Date.now();
+    newTpl.name = newTpl.name + ' (副本)';
+    templateList.value.unshift(newTpl);
+    ElMessage.success('复制成功');
+};
+
+const handleDelete = (index) => {
+    ElMessageBox.confirm('确定删除该运费模板吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+    }).then(() => {
+        templateList.value.splice(index, 1);
+        ElMessage.success('删除成功');
+    });
+};
+
+const handleSaveSuccess = (data) => {
+    const index = templateList.value.findIndex(item => item.id === data.id);
+    if (index !== -1) {
+        // Update
+        templateList.value[index] = data;
+    } else {
+        // Add
+        templateList.value.unshift(data);
+    }
+};
+</script>

+ 68 - 0
src/views/mallOrder/all/components/add-package-dialog.vue

@@ -0,0 +1,68 @@
+<template>
+    <ele-modal
+        :width="600"
+        v-model="visible"
+        title="新增额外包裹"
+        @open="handleOpen"
+        :body-style="{ padding: '20px' }"
+    >
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+            <el-form-item label="物流公司" prop="company">
+                <el-select v-model="form.company" placeholder="请选择物流公司" style="width: 100%">
+                    <el-option label="顺丰速运" value="shunfeng" />
+                    <el-option label="圆通速递" value="yuantong" />
+                    <el-option label="中通快递" value="zhongtong" />
+                    <el-option label="韵达快递" value="yunda" />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="快递单号" prop="no">
+                <el-input v-model="form.no" placeholder="请输入快递单号" />
+            </el-form-item>
+            <el-form-item label="包裹备注" prop="remark">
+                <el-input v-model="form.remark" type="textarea" placeholder="请输入包裹备注" />
+            </el-form-item>
+        </el-form>
+        <template #footer>
+            <el-button @click="visible = false">取消</el-button>
+            <el-button type="primary" @click="handleSubmit">确定</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+
+const visible = defineModel({ type: Boolean });
+const formRef = ref(null);
+
+const form = reactive({
+    company: '',
+    no: '',
+    remark: ''
+});
+
+const rules = {
+    company: [{ required: true, message: '请选择物流公司', trigger: 'change' }],
+    no: [{ required: true, message: '请输入快递单号', trigger: 'blur' }]
+};
+
+const handleOpen = () => {
+    form.company = '';
+    form.no = '';
+    form.remark = '';
+};
+
+const handleSubmit = () => {
+    formRef.value.validate((valid) => {
+        if (valid) {
+            EleMessage.success('添加成功');
+            visible.value = false;
+        }
+    });
+};
+
+defineExpose({
+    handleOpen: () => { visible.value = true; }
+});
+</script>

+ 200 - 0
src/views/mallOrder/all/components/order-base-info.vue

@@ -0,0 +1,200 @@
+<template>
+    <div class="order-base-info">
+        <ele-card header="订单基本信息">
+            <div class="info-layout">
+                <!-- Left Column: Order Info -->
+                <div class="info-column">
+                    <div class="info-item">
+                        <span class="label">订单编号:</span>
+                        <span class="value">{{ detail.orderId }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">订单来源:</span>
+                        <span class="value">{{ detail.source }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">支付渠道:</span>
+                        <span class="value">{{ detail.payChannel }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">交易状态:</span>
+                        <span class="value">{{ detail.status }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">平台备注:</span>
+                        <span class="value">{{ detail.remark || '正常发货' }}</span>
+                    </div>
+                </div>
+
+                <!-- Center Column: User/Receiver Info -->
+                <div class="info-column">
+                    <div class="info-item">
+                        <span class="label">用户名:</span>
+                        <span class="value">{{ detail.buyer?.name }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">收 件 人:</span>
+                        <span class="value">{{ detail.receiver?.name }}</span>
+                        <el-button link type="success" class="ml-2" @click="$emit('edit-receiver')">修改</el-button>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">电 话:</span>
+                        <span class="value">{{ detail.receiver?.phone }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">地 址:</span>
+                        <span class="value">{{ detail.receiver?.address }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">用户备注:</span>
+                        <span class="value">{{ detail.buyerNote || '无' }}</span>
+                    </div>
+                    <div class="info-item warning-text" v-if="detail.shortageNote">
+                        <span class="label" style="color: #f56c6c;">如遇缺货:</span>
+                        <span class="value" style="color: #f56c6c;">{{ detail.shortageNote }}</span>
+                    </div>
+                </div>
+
+                <!-- Right Column: Logistics Info -->
+                <div class="info-column">
+                    <div class="info-item">
+                        <span class="label">发货物流:</span>
+                        <span class="value">{{ detail.logistics?.company || '-' }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">发货单号:</span>
+                        <span class="value">{{ detail.logistics?.no || '-' }}</span>
+
+                        <el-popover placement="bottom" title="物流动态" :width="300" trigger="hover">
+                            <template #reference>
+                                <el-button link type="success" class="ml-2" v-if="detail.logistics?.no"
+                                    @click="viewLogistics">查看</el-button>
+                            </template>
+                            <el-timeline>
+                                <el-timeline-item v-for="(activity, index) in activities" :key="index"
+                                    :timestamp="activity.timestamp">
+                                    {{ activity.content }}
+                                </el-timeline-item>
+                            </el-timeline>
+                        </el-popover>
+                    </div>
+
+                    <div class="action-area mt-2">
+                        <el-button type="success" @click="$emit('add-package')">新增额外包裹 <el-icon class="el-icon--right">
+                                <Lightning />
+                            </el-icon></el-button>
+                    </div>
+
+                    <div class="status-tags mt-2">
+                        <el-tag :type="detail.isShipped ? 'success' : 'warning'" effect="dark" class="mr-2">
+                            {{ detail.isShipped ? '已发货' : '未发货' }}
+                        </el-tag>
+                        <el-tag :type="detail.isCompleted ? 'success' : 'warning'" effect="dark">
+                            {{ detail.isCompleted ? '已完成' : '未完成' }}
+                        </el-tag>
+                    </div>
+                </div>
+            </div>
+        </ele-card>
+    </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { Lightning } from '@element-plus/icons-vue';
+
+const props = defineProps({
+    detail: {
+        type: Object,
+        default: () => ({})
+    }
+});
+
+const emit = defineEmits(['add-package', 'edit-receiver']);
+
+const activities = [
+    {
+        content: '已签收,签收人是拍照签收',
+        timestamp: '2024-04-12 20:46',
+    },
+    {
+        content: '派送中',
+        timestamp: '2024-04-12 20:46',
+    },
+    {
+        content: '已发货',
+        timestamp: '2024-04-11 20:46',
+    },
+];
+
+const viewLogistics = () => {
+    // Logic to view full logistics
+};
+</script>
+
+<style lang="scss" scoped>
+.info-layout {
+    display: flex;
+    justify-content: space-between;
+    gap: 20px;
+
+    .info-column {
+        flex: 1;
+
+        .info-item {
+            margin-bottom: 12px;
+            font-size: 14px;
+            display: flex;
+
+            .label {
+                color: #606266;
+                width: 80px;
+                flex-shrink: 0;
+            }
+
+            .value {
+                color: #303133;
+                font-weight: 500;
+                flex: 1;
+            }
+        }
+
+        .warning-text {
+            color: #f56c6c;
+            font-weight: bold;
+        }
+    }
+}
+
+.status-box {
+    background-color: #fdf6ec;
+    border: 1px solid #faecd8;
+    color: #e6a23c;
+    padding: 10px;
+    border-radius: 4px;
+    font-size: 12px;
+    cursor: pointer;
+    margin-top: 10px;
+
+    .status-dot {
+        display: inline-block;
+        width: 6px;
+        height: 6px;
+        background-color: #e6a23c;
+        border-radius: 50%;
+        margin-right: 5px;
+    }
+}
+
+.ml-2 {
+    margin-left: 8px;
+}
+
+.mr-2 {
+    margin-right: 8px;
+}
+
+.mt-2 {
+    margin-top: 8px;
+}
+</style>

+ 202 - 0
src/views/mallOrder/all/components/order-detail.vue

@@ -0,0 +1,202 @@
+<template>
+    <ele-modal
+        :width="1200"
+        v-model="visible"
+        title="订单详情"
+        @open="handleOpen"
+        :body-style="{
+            maxHeight: '80vh',
+            overflow: 'auto',
+            padding: '20px',
+            backgroundColor: '#f5f7fa'
+        }"
+        top="5vh"
+    >
+        <!-- Status Steps -->
+        <ele-card class="mb-4">
+            <el-steps :active="activeStep" align-center finish-status="success">
+                <el-step title="提交订单" :description="detail.createTime" />
+                <el-step title="支付订单" :description="detail.payTime" />
+                <el-step title="平台发货" :description="detail.deliveryTime" />
+                <el-step title="确认收货" :description="detail.receiveTime" />
+                <el-step title="完成评价" :description="detail.finishTime" />
+            </el-steps>
+        </ele-card>
+
+        <!-- Order Base Info Component -->
+        <order-base-info 
+            :detail="detail" 
+            @add-package="openAddPackage"
+            @edit-receiver="handleEditReceiver"
+            class="mb-4"
+        />
+
+        <!-- Product List Component -->
+        <order-product-list 
+            :products="detail.products"
+            :payment="detail.payment"
+            class="mb-4"
+        />
+
+        <!-- Action Buttons Footer -->
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="visible = false">关闭</el-button>
+                <el-button type="success" @click="openPushSms">推送短信</el-button>
+                <el-button type="warning" @click="openRefund">缺货退款</el-button>
+            </div>
+        </template>
+
+        <!-- Sub Dialogs -->
+        <add-package-dialog ref="packageRef" />
+        <push-sms-dialog ref="smsRef" />
+        <refund-dialog ref="refundRef" />
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+import AddPackageDialog from './add-package-dialog.vue';
+import PushSmsDialog from './push-sms-dialog.vue';
+import RefundDialog from './refund-dialog.vue';
+import OrderBaseInfo from './order-base-info.vue';
+import OrderProductList from './order-product-list.vue';
+
+const visible = defineModel({ type: Boolean });
+const detail = ref({});
+const packageRef = ref(null);
+const smsRef = ref(null);
+const refundRef = ref(null);
+
+const activeStep = computed(() => {
+    const status = detail.value.status;
+    if (status === '等待买家付款') return 0;
+    if (status === '等待卖家发货') return 1;
+    if (status === '等待买家确认收货') return 2;
+    if (status === '交易成功') return 4;
+    return 1; // Default
+});
+
+const handleOpen = (data) => {
+    // Enhanced Mock Data logic
+    // In a real app, this would fetch from API using data.orderId
+    
+    // Base Mock Data
+    const baseData = {
+        orderId: '4066879393210936038',
+        createTime: '2024-10-04 12:01:09',
+        payTime: '2024-10-04 12:05:00',
+        deliveryTime: '2024-10-05 09:30:00',
+        receiveTime: '',
+        finishTime: '',
+        source: '微信',
+        payChannel: '微信支付',
+        status: '等待买家确认收货',
+        remark: '正常发货',
+        buyer: {
+            name: '书嗨565652323',
+            id: 'u123456'
+        },
+        receiver: {
+            name: '书嗨',
+            phone: '17512688011',
+            address: '河南省/鹤壁市/浚县/黄河路街道'
+        },
+        buyerNote: '发新一点的',
+        shortageNote: '缺货时与我电话沟通',
+        logistics: {
+            company: '韵达快递',
+            no: '463855119742',
+            status: '已发货'
+        },
+        isShipped: true,
+        isCompleted: false,
+        
+        // Payment Info
+        payment: {
+            total: 2.9,
+            shipping: 2.9,
+            discountTotal: 2.9,
+            redPacket: 2.9,
+            shareDiscount: 2.9
+        },
+        
+        // Products
+        products: [
+            {
+                image: 'https://img14.360buyimg.com/n0/jfs/t1/157997/3/36676/122176/65e975a0F98822998/5023348143493720.jpg',
+                title: '毛泽东思想和中国特色社会主义理论体系概论(2023年版)',
+                isbn: '9787040599039',
+                condition: '中等',
+                discount: 1,
+                recycleStatus: '正在回收',
+                refundStatus: '未确认收货',
+                qty: 1,
+                price: 2.9,
+                discountAmount: 0.9,
+                actualPayment: 2,
+                weight: 0.5
+            },
+            {
+                image: 'https://img14.360buyimg.com/n0/jfs/t1/157997/3/36676/122176/65e975a0F98822998/5023348143493720.jpg',
+                title: '毛泽东思想和中国特色社会主义理论体系概论(2023年版)',
+                isbn: '9787040599039',
+                condition: '中等',
+                discount: 1,
+                recycleStatus: '正在回收',
+                refundStatus: '未确认收货',
+                qty: 1,
+                price: 2.9,
+                discountAmount: 0,
+                actualPayment: 2.9,
+                weight: 0.5
+            }
+        ]
+    };
+
+    // If passed data has specific overrides, merge them (simple merge)
+    if (data && data.orderId) {
+        detail.value = { ...baseData, ...data };
+        // Ensure complex objects are not lost if not present in passed data
+        if (!data.products) detail.value.products = baseData.products;
+        if (!data.payment) detail.value.payment = baseData.payment;
+        if (!data.receiver) detail.value.receiver = baseData.receiver;
+    } else {
+        detail.value = baseData;
+    }
+};
+
+const openAddPackage = () => {
+    packageRef.value?.handleOpen();
+};
+
+const openPushSms = () => {
+    smsRef.value?.handleOpen(detail.value);
+};
+
+const openRefund = () => {
+    refundRef.value?.handleOpen(detail.value);
+};
+
+const handleEditReceiver = () => {
+    EleMessage.info('修改收货人信息功能待开发');
+};
+
+defineExpose({
+    handleOpen: (data) => {
+        visible.value = true;
+        handleOpen(data);
+    }
+});
+</script>
+
+<style scoped>
+.mb-4 {
+    margin-bottom: 16px;
+}
+.dialog-footer {
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 288 - 0
src/views/mallOrder/all/components/order-item.vue

@@ -0,0 +1,288 @@
+<template>
+    <div class="order-item">
+        <!-- Order Header -->
+        <div class="order-header">
+            <el-checkbox v-model="order.checked" style="margin-right: 12px;" />
+            <span class="mr-4">订单编号: {{ order.orderId }} <el-button link type="primary" size="small"
+                    @click="handleCopy(order.orderId)">复制</el-button></span>
+            <span class="mr-4">创建时间: {{ order.createTime }}</span>
+            <span class="mr-4">支付渠道: {{ order.payChannel }}</span>
+            <span class="mr-4">订单来源: {{ order.source }}</span>
+            <span class="flag-icon" v-if="order.flag"><el-icon>
+                    <Flag />
+                </el-icon></span>
+            <span class="urge-tag" v-if="order.isUrge">催发货</span>
+        </div>
+
+        <!-- Order Body -->
+        <div class="order-body">
+            <!-- Products Column -->
+            <div class="col-products-wrapper">
+                <div v-for="(product, idx) in order.products" :key="idx" class="product-row">
+                    <div class="col-product product-info mr-2">
+                        <el-image :src="product.image" class="product-img" fit="cover" />
+                        <div class="product-detail">
+                            <div class="product-title">{{ product.title }}</div>
+                            <div class="product-isbn">ISBN: <span class="link">{{ product.isbn }}</span>
+                                <el-icon class="cursor-pointer" @click="handleCopy(product.isbn)">
+                                    <CopyDocument />
+                                </el-icon> (品相: {{ product.condition }})
+                            </div>
+                            <div class="product-tags">
+                                <span class="tag">回收折扣: {{ product.discount }}折</span>
+                                <span class="tag success">(回收状态: {{ product.recycleStatus }})</span>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-price">¥ {{ product.price }}</div>
+                    <div class="col-qty">x{{ product.qty }}</div>
+                    <div class="col-aftersales">
+                        <span
+                            :class="{ 'text-blue': product.refundStatus === '退款成功', 'text-red': product.refundStatus === '退款取消' }">{{
+                                product.refundStatus }}</span>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Merged Columns -->
+            <div class="col-merged col-buyer">
+                <div class="buyer-info">
+                    <el-avatar :size="30" :src="order.buyer.avatar" />
+                    <div class="buyer-name">{{ order.buyer.name }}</div>
+                    <div class="buyer-phone">{{ order.buyer.phone }}</div>
+                </div>
+            </div>
+            <div class="col-merged col-payment">
+                <div class="payment-amount">¥ {{ order.payment.total }}</div>
+                <div class="shipping-fee">(含邮费: ¥ {{ order.payment.shipping }})</div>
+            </div>
+            <div class="col-merged col-logistics">
+                <div>{{ order.logistics.company }}</div>
+                <div>{{ order.logistics.no }}</div>
+            </div>
+            <div class="col-merged col-status">
+                <div :class="getStatusColor(order.status)">{{ order.status }}</div>
+                <el-button link type="primary" @click="$emit('view-detail', order)">[查看详情]</el-button>
+            </div>
+            <div class="col-merged col-action">
+                <el-button link type="primary" @click="$emit('push-sms', order)">[推送短信]</el-button>
+                <el-button link type="warning" @click="$emit('refund', order)">[缺货退款]</el-button>
+            </div>
+        </div>
+
+        <!-- Buyer Note -->
+        <div class="buyer-note" v-if="order.buyerNote">
+            <span class="note-label">买家备注:</span>
+            <span class="note-content">{{ order.buyerNote }}</span>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { Flag, CopyDocument } from '@element-plus/icons-vue';
+import { EleMessage } from 'ele-admin-plus/es';
+import { useClipboard } from '@vueuse/core';
+
+const props = defineProps({
+    order: {
+        type: Object,
+        required: true
+    }
+});
+
+defineEmits(['view-detail', 'push-sms', 'refund']);
+
+const { copy } = useClipboard();
+
+const handleCopy = async (text) => {
+    try {
+        await copy(text);
+        EleMessage.success('复制成功');
+    } catch (e) {
+        EleMessage.error('复制失败');
+    }
+};
+
+const getStatusColor = (status) => {
+    if (status === '退款成功') return 'text-red';
+    if (status === '等待买家确认收货') return 'text-red';
+    return '';
+};
+</script>
+
+<style lang="scss" scoped>
+.order-item {
+    border: 1px solid #ebeef5;
+    margin-bottom: 15px;
+
+    .order-header {
+        background-color: #f0f9eb; // Light blue/greenish background like screenshot
+        padding: 8px 15px;
+        font-size: 13px;
+        color: #606266;
+        display: flex;
+        align-items: center;
+
+        .urge-tag {
+            background-color: #f56c6c;
+            color: #fff;
+            padding: 1px 5px;
+            border-radius: 2px;
+            font-size: 12px;
+            margin-left: 10px;
+        }
+    }
+
+    .order-body {
+        display: flex;
+        border-top: 1px solid #ebeef5;
+
+        .col-products-wrapper {
+            flex: 4; // Takes up 4 columns worth of space (Product, Price, Qty, Aftersales)
+            display: flex;
+            flex-direction: column;
+        }
+
+        .product-row {
+            display: flex;
+            border-bottom: 1px solid #ebeef5;
+
+            &:last-child {
+                border-bottom: none;
+            }
+
+            padding: 10px 0;
+
+            .col-product {
+                flex: 1.5;
+            }
+
+            .col-price {
+                flex: 0.5;
+            }
+
+            .col-qty {
+                flex: 0.5;
+            }
+
+            .col-aftersales {
+                flex: 0.5;
+            }
+        }
+
+        .col-merged {
+            border-left: 1px solid #ebeef5;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            padding: 10px;
+            text-align: center;
+            font-size: 13px;
+        }
+    }
+
+    .buyer-note {
+        background-color: #fff0f0;
+        padding: 8px 15px;
+        font-size: 12px;
+        color: #f56c6c;
+        border-top: 1px solid #ebeef5;
+
+        .note-label {
+            font-weight: bold;
+            margin-right: 5px;
+        }
+    }
+}
+
+// Column Widths
+.col-product {
+    flex: 1.5;
+    text-align: left !important;
+    padding-left: 15px !important;
+}
+
+.col-price {
+    flex: 0.5;
+}
+
+.col-qty {
+    flex: 0.5;
+}
+
+.col-aftersales {
+    flex: 0.5;
+}
+
+.col-buyer {
+    flex: 0.8;
+}
+
+.col-payment {
+    flex: 0.8;
+}
+
+.col-logistics {
+    flex: 1;
+}
+
+.col-status {
+    flex: 0.8;
+}
+
+.col-action {
+    flex: 0.8;
+}
+
+// Product Styles
+.product-info {
+    display: flex;
+    align-items: flex-start;
+
+    .product-img {
+        width: 60px;
+        height: 60px;
+        margin-right: 10px;
+        border-radius: 2px;
+    }
+
+    .product-detail {
+        font-size: 13px;
+
+        .product-title {
+            color: #303133;
+            margin-bottom: 4px;
+        }
+
+        .product-isbn {
+            color: #909399;
+            margin-bottom: 4px;
+
+            .link {
+                color: #409eff;
+                cursor: pointer;
+            }
+        }
+
+        .product-tags {
+            .tag {
+                color: #67c23a;
+                margin-right: 5px;
+
+                &.success {
+                    color: #67c23a;
+                }
+            }
+        }
+    }
+}
+
+.text-red {
+    color: #f56c6c;
+}
+
+.text-blue {
+    color: #409eff;
+}
+</style>

+ 202 - 0
src/views/mallOrder/all/components/order-product-list.vue

@@ -0,0 +1,202 @@
+<template>
+    <div class="order-product-list">
+        <ele-card header="商品详情">
+            <el-table :data="products" border style="width: 100%">
+                <!-- Image -->
+                <el-table-column label="图片" width="80" align="center">
+                    <template #default="{ row }">
+                        <el-image 
+                            :src="row.image" 
+                            style="width: 60px; height: 60px" 
+                            fit="cover" 
+                            :preview-src-list="[row.image]"
+                            preview-teleported
+                        />
+                    </template>
+                </el-table-column>
+
+                <!-- Product Info -->
+                <el-table-column label="商品信息" min-width="250">
+                    <template #default="{ row }">
+                        <div class="product-info">
+                            <div class="title">{{ row.title }}</div>
+                            <div class="meta">ISBN: {{ row.isbn }}</div>
+                            <div class="tags">
+                                <span class="discount-tag">回收折扣: {{ row.discount }}折</span>
+                                <span class="status-tag">(回收状态: {{ row.recycleStatus }})</span>
+                            </div>
+                        </div>
+                    </template>
+                </el-table-column>
+
+                <!-- Specs -->
+                <el-table-column label="规格" width="100" align="center">
+                    <template #default="{ row }">
+                        <span>({{ row.condition }})</span>
+                    </template>
+                </el-table-column>
+
+                <!-- Status -->
+                <el-table-column label="状态" width="120" align="center">
+                    <template #default="{ row }">
+                        <span :class="getStatusColor(row.refundStatus)">{{ row.refundStatus || '未确认收货' }}</span>
+                    </template>
+                </el-table-column>
+
+                <!-- Quantity -->
+                <el-table-column label="数量" width="80" align="center">
+                    <template #default="{ row }">
+                        <span>x{{ row.qty }}</span>
+                    </template>
+                </el-table-column>
+
+                <!-- Unit Price -->
+                <el-table-column label="单价" width="100" align="center">
+                    <template #default="{ row }">
+                        <span>¥ {{ row.price }}</span>
+                    </template>
+                </el-table-column>
+
+                <!-- Discount -->
+                <el-table-column label="优惠" width="120" align="center">
+                    <template #default="{ row }">
+                        <div v-if="row.discountAmount">
+                            <div>惊喜红包</div>
+                            <div>¥ {{ row.discountAmount }}</div>
+                        </div>
+                        <div v-else>¥ 0</div>
+                    </template>
+                </el-table-column>
+
+                <!-- Actual Payment -->
+                <el-table-column label="实收款" width="100" align="center">
+                    <template #default="{ row }">
+                        <span>¥ {{ row.actualPayment }}</span>
+                    </template>
+                </el-table-column>
+
+                <!-- Weight -->
+                <el-table-column label="重量" width="80" align="center">
+                    <template #default="{ row }">
+                        <span>{{ row.weight }}kg</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <!-- Summary Footer -->
+            <div class="summary-footer">
+                <div class="summary-row" v-if="payment.discountTotal > 0">
+                    <span>余额优惠:</span>
+                    <span class="amount red">¥ {{ payment.discountTotal }}</span>
+                </div>
+                <div class="summary-row" v-if="payment.redPacket > 0">
+                    <span>惊喜红包:</span>
+                    <span class="amount red">¥ {{ payment.redPacket }}</span>
+                </div>
+                <div class="summary-row" v-if="payment.shareDiscount > 0">
+                    <span>分享降价:</span>
+                    <span class="amount red">¥ {{ payment.shareDiscount }}</span>
+                </div>
+                <div class="summary-row">
+                    <span>运费:</span>
+                    <span class="amount red">¥ {{ payment.shipping }}</span>
+                </div>
+                <div class="summary-row total">
+                    <span>订单实付金额:</span>
+                    <span class="amount red large">¥ {{ payment.total }}</span>
+                </div>
+            </div>
+        </ele-card>
+    </div>
+</template>
+
+<script setup>
+const props = defineProps({
+    products: {
+        type: Array,
+        default: () => []
+    },
+    payment: {
+        type: Object,
+        default: () => ({
+            total: 0,
+            shipping: 0,
+            discountTotal: 0,
+            redPacket: 0,
+            shareDiscount: 0
+        })
+    }
+});
+
+const getStatusColor = (status) => {
+    if (status === '退款成功') return 'text-green'; // Or red based on design
+    if (status === '未确认收货') return 'text-green';
+    return '';
+};
+</script>
+
+<style lang="scss" scoped>
+.product-info {
+    font-size: 13px;
+    .title {
+        color: #333;
+        margin-bottom: 4px;
+        line-height: 1.4;
+    }
+    .meta {
+        color: #999;
+        margin-bottom: 4px;
+    }
+    .tags {
+        font-size: 12px;
+        .discount-tag {
+            color: #67c23a;
+            margin-right: 5px;
+        }
+        .status-tag {
+            color: #67c23a;
+        }
+    }
+}
+
+.text-green { color: #67c23a; }
+.text-red { color: #f56c6c; }
+
+.summary-footer {
+    margin-top: 15px;
+    padding: 15px;
+    background-color: #f8fcfb; // Light greenish tint matching the screenshot bottom area
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+    
+    .summary-row {
+        margin-bottom: 5px;
+        font-size: 14px;
+        color: #606266;
+        
+        .amount {
+            margin-left: 10px;
+            font-weight: 500;
+            width: 80px;
+            text-align: right;
+            display: inline-block;
+            
+            &.red {
+                color: #f56c6c;
+            }
+            
+            &.large {
+                font-size: 16px;
+                font-weight: bold;
+            }
+        }
+        
+        &.total {
+            margin-top: 5px;
+            padding-top: 5px;
+            border-top: 1px dashed #e4e7ed;
+        }
+    }
+}
+</style>

+ 74 - 0
src/views/mallOrder/all/components/order-table-header.vue

@@ -0,0 +1,74 @@
+<template>
+    <div class="custom-table-header">
+        <div class="th-item col-product">商品信息</div>
+        <div class="th-item col-price">单价</div>
+        <div class="th-item col-qty">数量</div>
+        <div class="th-item col-aftersales">售后</div>
+        <div class="th-item col-buyer">买家</div>
+        <div class="th-item col-payment">实收款</div>
+        <div class="th-item col-logistics">发货物流</div>
+        <div class="th-item col-status">交易状态</div>
+        <div class="th-item col-action">操作</div>
+    </div>
+</template>
+
+<script setup>
+</script>
+
+<style lang="scss" scoped>
+.custom-table-header {
+    display: flex;
+    background-color: #f5f7fa;
+    padding: 10px 0;
+    font-weight: bold;
+    color: #606266;
+    margin-top: 10px;
+    border: 1px solid #ebeef5;
+    border-bottom: none;
+
+    .th-item {
+        text-align: center;
+        padding: 0 5px;
+        font-size: 13px;
+    }
+}
+
+// Column Widths - Keep consistent with OrderItem
+.col-product {
+    flex: 1.5;
+    text-align: left !important;
+    padding-left: 15px !important;
+}
+
+.col-price {
+    flex: 0.5;
+}
+
+.col-qty {
+    flex: 0.5;
+}
+
+.col-aftersales {
+    flex: 0.5;
+}
+
+.col-buyer {
+    flex: 0.8;
+}
+
+.col-payment {
+    flex: 0.8;
+}
+
+.col-logistics {
+    flex: 1;
+}
+
+.col-status {
+    flex: 0.8;
+}
+
+.col-action {
+    flex: 0.8;
+}
+</style>

+ 116 - 0
src/views/mallOrder/all/components/page-search.vue

@@ -0,0 +1,116 @@
+<template>
+    <ele-card :body-style="{ paddingBottom: '0' }">
+        <el-form :model="where" label-width="110px" @keyup.enter="search">
+            <el-row :gutter="15">
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="订单编号">
+                        <el-input v-model="where.orderId" placeholder="请输入" clearable />
+                    </el-form-item>
+                </el-col>
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="收件人姓名">
+                        <el-input v-model="where.receiverName" placeholder="请输入" clearable />
+                    </el-form-item>
+                </el-col>
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="收件人手机号">
+                        <el-input v-model="where.receiverPhone" placeholder="请输入" clearable />
+                    </el-form-item>
+                </el-col>
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="发货物流单号">
+                        <el-input v-model="where.logisticsNo" placeholder="请输入" clearable />
+                    </el-form-item>
+                </el-col>
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="退货物流单号">
+                        <el-input v-model="where.returnLogisticsNo" placeholder="请输入" clearable />
+                    </el-form-item>
+                </el-col>
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="全部订单状态">
+                        <el-select v-model="where.status" placeholder="请选择" clearable>
+                            <el-option v-for="item in statusDicts" :key="item.dictValue" :label="item.dictLabel"
+                                :value="item.dictValue" />
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="搜索备注关键字">
+                        <el-input v-model="where.keyword" placeholder="请输入" clearable />
+                    </el-form-item>
+                </el-col>
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="催发货订单">
+                        <el-select v-model="where.urgeStatus" placeholder="请选择" clearable>
+                            <el-option label="催发货" value="1" />
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+                <el-col :lg="6" :md="12">
+                    <el-form-item label="下单时间">
+                        <el-date-picker v-model="dateRange" type="daterange" range-separator="到"
+                            start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD"
+                            style="width: 100%" @change="handleDateChange" />
+                    </el-form-item>
+                </el-col>
+
+                <el-col :lg="6" :md="12" class="mb-4">
+                    <el-button type="primary" @click="search">查询</el-button>
+                    <el-button @click="reset">重置</el-button>
+                </el-col>
+            </el-row>
+        </el-form>
+    </ele-card>
+</template>
+
+<script setup>
+import { reactive, ref } from 'vue';
+import { useDictData } from '@/utils/use-dict-data';
+
+const emit = defineEmits(['search']);
+
+// 字典
+const [statusDicts] = useDictData(['order_status']);
+
+const where = reactive({
+    orderId: '',
+    receiverName: '',
+    receiverPhone: '',
+    logisticsNo: '',
+    returnLogisticsNo: '',
+    status: '',
+    keyword: '',
+    urgeStatus: '',
+    startTime: '',
+    endTime: ''
+});
+
+const dateRange = ref([]);
+
+const handleDateChange = (val) => {
+    if (val && val.length === 2) {
+        where.startTime = val[0];
+        where.endTime = val[1];
+    } else {
+        where.startTime = '';
+        where.endTime = '';
+    }
+};
+
+const search = () => {
+    emit('search', { ...where });
+};
+
+const reset = () => {
+    Object.keys(where).forEach(key => where[key] = '');
+    dateRange.value = [];
+    search();
+};
+</script>
+
+<style scoped>
+.mb-4 {
+    margin-bottom: 16px;
+}
+</style>

+ 209 - 0
src/views/mallOrder/all/components/push-sms-dialog.vue

@@ -0,0 +1,209 @@
+<template>
+    <ele-modal
+        form
+        :width="800"
+        v-model="visible"
+        title="推送短信"
+        @open="handleOpen"
+    >
+        <el-form
+            ref="formRef"
+            :model="form"
+            :rules="rules"
+            label-width="80px"
+            @submit.prevent=""
+        >
+            <el-form-item label="短信类型" prop="type">
+                <el-radio-group v-model="form.type" @change="handleChangeType">
+                    <el-radio
+                        :value="item.type"
+                        v-for="item in smsContentList"
+                        :key="item.type"
+                        >{{ item.typeName }}</el-radio
+                    >
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label="短信预览" prop="remark">
+                <el-input
+                    :rows="4"
+                    type="textarea"
+                    v-model="form.remark"
+                    disabled
+                    placeholder="请输入短信预览"
+                />
+            </el-form-item>
+            <el-form-item label="推送历史" prop="records">
+                <ele-data-table
+                    row-key="id"
+                    :columns="columns"
+                    :data="smsRecords"
+                    height="200px"
+                    :show-overflow-tooltip="true"
+                />
+            </el-form-item>
+        </el-form>
+
+        <template #footer>
+            <el-button @click="handleCancel">关闭</el-button>
+            <el-button type="primary" @click="handleSubmit">确定</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, getCurrentInstance } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(['success']);
+
+/** 弹窗是否打开 */
+const visible = defineModel({ type: Boolean });
+
+/** 表单引用 */
+const formRef = ref();
+
+/** SMS记录数据 */
+const smsRecords = ref([]);
+
+/** 短信内容列表 */
+const smsContentList = ref([]);
+
+const form = reactive({
+    orderId: null,
+    type: '',
+    remark: ''
+});
+
+/** 短信类型变化 */
+const handleChangeType = (val) => {
+    const found = smsContentList.value.find((item) => item.type === val);
+    if (found) {
+        form.remark = found.smsContent;
+    }
+};
+
+/** 表单验证规则 */
+const rules = reactive({
+    type: [
+        {
+            required: true,
+            message: '请选择短信类型',
+            trigger: 'change'
+        }
+    ],
+    remark: [
+        {
+            required: true,
+            message: '请输入短信预览',
+            type: 'string',
+            trigger: 'blur'
+        }
+    ]
+});
+
+// 表格列配置
+const columns = reactive([
+    { label: '发送人', prop: 'createName', width: 100, align: 'center' },
+    { label: '发送内容', prop: 'smsContent', align: 'center', minWidth: 200 },
+    { label: '发送时间', prop: 'createTime', width: 160, align: 'center' },
+    {
+        label: '发送状态',
+        prop: 'status',
+        width: 100,
+        align: 'center',
+        formatter: (row) => (row.status == 1 ? '成功' : '失败')
+    }
+]);
+
+/** 关闭弹窗 */
+const handleCancel = () => {
+    visible.value = false;
+    resetForm();
+};
+
+/** 重置表单 */
+const resetForm = () => {
+    form.orderId = null;
+    form.type = '';
+    form.remark = '';
+    formRef.value?.resetFields();
+    smsRecords.value = [];
+};
+
+/** 弹窗打开事件 */
+const handleOpen = (row) => {
+    resetForm();
+    // 模拟获取数据,实际开发中可以使用 row.orderId 调用接口
+    if (row) {
+        form.orderId = row.orderId || 123;
+        getSmsLogInfo(form.orderId);
+    } else {
+        // 如果没有 row 传入,也可以默认加载 mock 数据方便预览
+        getSmsLogInfo(123);
+    }
+    visible.value = true;
+};
+
+/** 获取短信记录信息 (Mock) */
+const getSmsLogInfo = (orderId) => {
+    // 模拟 API 请求延迟
+    setTimeout(() => {
+        // Mock data
+        smsContentList.value = [
+            { type: 'urge', typeName: '催发货提醒', smsContent: '亲,您的订单已发货,请注意查收。' },
+            { type: 'receive', typeName: '收货提醒', smsContent: '亲,您的宝贝已送达,请确认收货。' },
+            { type: 'good_review', typeName: '好评提醒', smsContent: '亲,如果您对宝贝满意,请给个好评哦~' }
+        ];
+
+        smsRecords.value = [
+            {
+                id: 1,
+                createName: '管理员',
+                smsContent: '亲,您的订单已发货,请注意查收。',
+                createTime: '2023-10-27 10:00:00',
+                status: 1
+            },
+            {
+                id: 2,
+                createName: '客服小美',
+                smsContent: '亲,您的宝贝已送达,请确认收货。',
+                createTime: '2023-10-28 14:30:00',
+                status: 1
+            }
+        ];
+    }, 100);
+};
+
+/** 提交表单 */
+const handleSubmit = () => {
+    formRef.value?.validate(async (valid) => {
+        if (!valid) return;
+        
+        // 模拟提交
+        setTimeout(() => {
+            EleMessage.success('短信发送成功');
+            // 添加一条记录到 mock 列表,模拟实时更新
+            smsRecords.value.unshift({
+                id: Date.now(),
+                createName: '当前用户',
+                smsContent: form.remark,
+                createTime: new Date().toLocaleString(),
+                status: 1
+            });
+            // 这里的业务逻辑是发送后不一定马上关闭,或者可以关闭
+            // 参考 send-SMS.vue 是关闭的
+            handleCancel();
+            emit('success');
+        }, 500);
+    });
+};
+
+defineExpose({
+    handleOpen
+});
+</script>
+
+<style scoped>
+/* 可以在这里添加样式微调 */
+</style>

+ 234 - 0
src/views/mallOrder/all/components/refund-dialog.vue

@@ -0,0 +1,234 @@
+<template>
+    <ele-modal
+        :width="800"
+        v-model="visible"
+        title="缺货退款"
+        @open="handleOpen"
+        :body-style="{ padding: '20px' }"
+    >
+        <div class="refund-container">
+            <div class="section-title">选择要退款的商品及数量</div>
+            
+            <el-table
+                ref="tableRef"
+                :data="order?.products || []"
+                style="width: 100%; border: 1px solid #ebeef5; border-bottom: none;"
+                @selection-change="handleSelectionChange"
+                header-row-class-name="custom-header"
+            >
+                <el-table-column type="selection" width="55" />
+                <el-table-column label="商品信息">
+                    <template #default="{ row }">
+                        <div class="product-info">
+                            <el-image :src="row.image" class="product-img" fit="cover" />
+                            <span class="product-title">{{ row.title }}</span>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column label="数量" width="150">
+                    <template #default="{ row }">
+                        <el-input-number 
+                            v-model="row.refundQty" 
+                            :min="1" 
+                            :max="row.qty" 
+                            size="small" 
+                            controls-position="right"
+                            style="width: 100px;"
+                        />
+                    </template>
+                </el-table-column>
+                <el-table-column label="单价" width="120">
+                    <template #default="{ row }">
+                        <span>¥{{ row.price }}</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <!-- Fill empty rows to mimic design height if needed, but el-table auto height is better. 
+                 Design shows empty grid lines, which el-table does by default if data is short? No.
+                 We'll stick to standard table. -->
+
+            <div class="refund-summary mt-4">
+                <div class="summary-item">
+                    <span class="label">退还商品:</span>
+                    <span class="value">{{ productRefundAmount.toFixed(2) }} 元</span>
+                    <span class="sub-text">(共{{ selectedCount }}件)</span>
+                </div>
+                <div class="summary-item items-center">
+                    <span class="label">退还运费:</span>
+                    <el-input 
+                        v-model="shippingRefund" 
+                        style="width: 100px; margin-right: 10px;" 
+                        placeholder="请输入"
+                    />
+                    <span class="label">元</span>
+                    <span class="warning-text">(不超过{{ maxShippingFee }}元)</span>
+                </div>
+                <div class="summary-item">
+                    <span class="label">退款总额:</span>
+                    <span class="total-amount">{{ totalRefundAmount.toFixed(2) }} 元</span>
+                    <span class="sub-text">(原订单总额{{ originalTotal.toFixed(2) }}元 新订单总额{{ newTotal.toFixed(2) }}元)</span>
+                </div>
+                <div class="summary-item mt-2">
+                    <el-checkbox v-model="sendCoupon">
+                        <span class="font-bold">发送“缺货退款购书红包”</span>
+                    </el-checkbox>
+                </div>
+            </div>
+        </div>
+
+        <template #footer>
+            <el-button @click="visible = false">取消</el-button>
+            <el-button type="primary" @click="handleSubmit">确定并退款</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, computed, watch } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+
+const visible = defineModel({ type: Boolean });
+const order = ref(null);
+const selectedRows = ref([]);
+const shippingRefund = ref(0);
+const sendCoupon = ref(true);
+const maxShippingFee = ref(5);
+
+const handleOpen = (row) => {
+    // Deep copy to avoid mutating original list directly until save
+    if (row) {
+        order.value = JSON.parse(JSON.stringify(row));
+        // Initialize refundQty for each product
+        if (order.value.products) {
+            order.value.products.forEach(p => {
+                p.refundQty = p.qty; // Default to max qty
+            });
+        }
+        shippingRefund.value = 0;
+        sendCoupon.value = true;
+        selectedRows.value = [];
+    }
+    visible.value = true;
+};
+
+const handleSelectionChange = (val) => {
+    selectedRows.value = val;
+};
+
+// Computed
+const productRefundAmount = computed(() => {
+    return selectedRows.value.reduce((sum, item) => {
+        return sum + (item.price * (item.refundQty || 0));
+    }, 0);
+});
+
+const selectedCount = computed(() => {
+    return selectedRows.value.reduce((sum, item) => {
+        return sum + (item.refundQty || 0);
+    }, 0);
+});
+
+const totalRefundAmount = computed(() => {
+    const shipping = parseFloat(shippingRefund.value) || 0;
+    return productRefundAmount.value + shipping;
+});
+
+const originalTotal = computed(() => {
+    return order.value?.payment?.total || 0;
+});
+
+const newTotal = computed(() => {
+    const val = originalTotal.value - totalRefundAmount.value;
+    return val > 0 ? val : 0;
+});
+
+const handleSubmit = () => {
+    if (selectedRows.value.length === 0 && parseFloat(shippingRefund.value) <= 0) {
+        EleMessage.warning('请选择退款商品或输入退款运费');
+        return;
+    }
+    
+    const shipping = parseFloat(shippingRefund.value) || 0;
+    if (shipping > maxShippingFee.value) {
+        EleMessage.error(`退还运费不能超过${maxShippingFee.value}元`);
+        return;
+    }
+
+    EleMessage.success('退款申请已提交');
+    visible.value = false;
+};
+
+defineExpose({
+    handleOpen
+});
+</script>
+
+<style scoped lang="scss">
+.refund-container {
+    .section-title {
+        font-weight: bold;
+        margin-bottom: 10px;
+        font-size: 14px;
+        color: #303133;
+    }
+    
+    .product-info {
+        display: flex;
+        align-items: center;
+        .product-img {
+            width: 50px;
+            height: 50px;
+            margin-right: 10px;
+            border-radius: 4px;
+        }
+        .product-title {
+            font-size: 13px;
+            line-height: 1.4;
+        }
+    }
+
+    .refund-summary {
+        font-size: 14px;
+        color: #606266;
+        
+        .summary-item {
+            display: flex;
+            align-items: center;
+            margin-bottom: 10px;
+            line-height: 24px;
+            
+            .label {
+                font-weight: bold;
+                color: #303133;
+            }
+            
+            .value {
+                color: #303133;
+                font-weight: bold;
+            }
+            
+            .total-amount {
+                color: #f56c6c;
+                font-weight: bold;
+                font-size: 16px;
+            }
+            
+            .sub-text {
+                color: #909399;
+                margin-left: 5px;
+                font-size: 13px;
+            }
+            
+            .warning-text {
+                color: #303133;
+                font-weight: bold;
+            }
+        }
+    }
+}
+:deep(.custom-header) th {
+    background-color: #f5f7fa !important;
+    color: #606266;
+    font-weight: normal;
+}
+</style>

+ 198 - 0
src/views/mallOrder/all/index.vue

@@ -0,0 +1,198 @@
+<template>
+    <ele-page flex-table>
+        <page-search @search="handleSearch" />
+
+        <ele-card :body-style="{ padding: '10px' }">
+            <!-- Tabs -->
+            <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+                <el-tab-pane label="全部订单状态" name="all" />
+                <el-tab-pane label="等待买家付款" name="1" />
+                <el-tab-pane label="等待卖家发货" name="2" />
+                <el-tab-pane label="等待买家确认收货" name="3" />
+                <el-tab-pane label="交易成功" name="4" />
+                <el-tab-pane label="退款成功" name="5" />
+                <el-tab-pane label="退款中" name="6" />
+                <el-tab-pane label="订单取消" name="7" />
+                <el-tab-pane label="预警订单/催发货" name="warning" />
+            </el-tabs>
+
+            <!-- Custom Table Header -->
+            <order-table-header />
+
+            <!-- Order List -->
+            <div class="order-list" v-loading="loading">
+                <order-item v-for="order in list" :key="order.orderId" :order="order" @view-detail="openDetail"
+                    @push-sms="openSms" @refund="openRefund" />
+            </div>
+
+            <!-- Pagination -->
+            <div class="pagination-wrapper">
+                <el-pagination v-model:current-page="pageParams.page" v-model:page-size="pageParams.limit"
+                    :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
+                    @current-change="handlePageChange" />
+            </div>
+        </ele-card>
+
+        <!-- Dialogs -->
+        <order-detail ref="detailRef" />
+        <push-sms-dialog ref="smsRef" />
+        <refund-dialog ref="refundRef" />
+        <add-package-dialog ref="packageRef" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import PageSearch from './components/page-search.vue';
+import OrderDetail from './components/order-detail.vue';
+import PushSmsDialog from './components/push-sms-dialog.vue';
+import RefundDialog from './components/refund-dialog.vue';
+import AddPackageDialog from './components/add-package-dialog.vue';
+import OrderTableHeader from './components/order-table-header.vue';
+import OrderItem from './components/order-item.vue';
+
+const activeTab = ref('all');
+const loading = ref(false);
+const total = ref(10);
+const pageParams = reactive({
+    page: 1,
+    limit: 10
+});
+
+const list = ref([
+    {
+        orderId: '4066879393210936038',
+        createTime: '2024-10-04 12:01:09',
+        payChannel: '微信支付',
+        source: '微信',
+        flag: true,
+        isUrge: false,
+        checked: false,
+        buyer: {
+            avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
+            name: '用户44541',
+            phone: '1894984dsaddq'
+        },
+        payment: {
+            total: 2.9,
+            shipping: 0
+        },
+        logistics: {
+            company: '韵达快递',
+            no: '46382090456249',
+            status: '已发货'
+        },
+        status: '退款成功',
+        buyerNote: '',
+        products: [
+            {
+                image: 'https://img14.360buyimg.com/n0/jfs/t1/157997/3/36676/122176/65e975a0F98822998/5023348143493720.jpg',
+                title: '毛泽东思想和中国特色社会主义理论体系概论(2023年版)',
+                isbn: '9787040599012',
+                condition: '中等',
+                price: 2.9,
+                qty: 1,
+                discount: 1,
+                recycleStatus: '正在回收',
+                refundStatus: '退款成功'
+            }
+        ]
+    },
+    {
+        orderId: '4066879393210936039',
+        createTime: '2024-10-04 12:06:09',
+        payChannel: '微信支付',
+        source: '微信',
+        flag: false,
+        isUrge: true,
+        checked: false,
+        buyer: {
+            avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
+            name: '用户44541',
+            phone: '1894984dsaddq'
+        },
+        payment: {
+            total: 5.8,
+            shipping: 0
+        },
+        logistics: {
+            company: '韵达快递',
+            no: '46382090456248',
+            status: '已发货'
+        },
+        status: '等待买家确认收货',
+        buyerNote: '您好,请确保书籍正版,且无破损、不缺页。请不要随书夹带任何票据,有问题留言不要打电话。请尽快发货,谢谢啦~',
+        products: [
+            {
+                image: 'https://img14.360buyimg.com/n0/jfs/t1/157997/3/36676/122176/65e975a0F98822998/5023348143493720.jpg',
+                title: '毛泽东思想和中国特色社会主义理论体系概论(2023年版)',
+                isbn: '9787040599012',
+                condition: '中等',
+                price: 2.9,
+                qty: 1,
+                discount: 1,
+                recycleStatus: '正在回收',
+                refundStatus: ''
+            },
+            {
+                image: 'https://img14.360buyimg.com/n0/jfs/t1/157997/3/36676/122176/65e975a0F98822998/5023348143493720.jpg',
+                title: '毛泽东思想和中国特色社会主义理论体系概论(2023年版)',
+                isbn: '9787040599012',
+                condition: '中等',
+                price: 2.9,
+                qty: 2,
+                discount: 1,
+                recycleStatus: '正在回收',
+                refundStatus: '退款取消'
+            }
+        ]
+    }
+]);
+
+const detailRef = ref(null);
+const smsRef = ref(null);
+const refundRef = ref(null);
+const packageRef = ref(null);
+
+const handleSearch = (params) => {
+    console.log('Search:', params);
+    // Mock reload
+    loading.value = true;
+    setTimeout(() => { loading.value = false; }, 500);
+};
+
+const handleTabClick = (tab) => {
+    console.log('Tab:', tab.props.name);
+    handleSearch({});
+};
+
+const handleSizeChange = (val) => {
+    pageParams.limit = val;
+    handleSearch({});
+};
+
+const handlePageChange = (val) => {
+    pageParams.page = val;
+    handleSearch({});
+};
+
+const openDetail = (row) => {
+    detailRef.value?.handleOpen(row);
+};
+
+const openSms = (row) => {
+    smsRef.value?.handleOpen(row);
+};
+
+const openRefund = (row) => {
+    refundRef.value?.handleOpen(row);
+};
+</script>
+
+<style lang="scss" scoped>
+.pagination-wrapper {
+    margin-top: 15px;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 46 - 0
src/views/mallOrder/complaint/components/complain-item.vue

@@ -0,0 +1,46 @@
+<template>
+    <div class="complain-item flex mt-2 pr-4">
+        <el-avatar shape="square" :size="40" fit="cover" :src="item.userType === '1' ? item.imgPath : Logo" />
+
+        <div class="flex-1 flex flex-col ml-2">
+            <div class="flex justify-between">
+                <ele-text size="small" type="info">{{
+                    item.userName
+                }}</ele-text>
+                <ele-text size="small" type="info">{{
+                    item.createTime || ''
+                }}</ele-text>
+            </div>
+            <div class="mt-2">
+                <ele-text>{{ item.description || '' }}</ele-text>
+            </div>
+            <!-- 显示图片列表 -->
+            <div v-if="item.imgList && item.imgList.length" class="mt-2 flex flex-wrap">
+                <el-image v-for="(img, imgIndex) in item.imgList" :key="imgIndex" style="
+                        width: 70px;
+                        height: 70px;
+                        border-radius: 5px;
+                        margin-right: 8px;
+                        margin-bottom: 8px;
+                    " :src="img" fit="cover" :preview-src-list="item.imgList" :initial-index="imgIndex"
+                    preview-teleported />
+            </div>
+        </div>
+    </div>
+    <el-divider style="margin: 10px 0" />
+</template>
+
+<script setup>
+import Logo from '@/assets/logo.png';
+defineProps({
+    item: {
+        type: Object,
+        default: () => ({
+            userType: '用户',
+            createTime: '',
+            description: '',
+            fileUrls: []
+        })
+    }
+});
+</script>

+ 149 - 0
src/views/mallOrder/complaint/components/negotiate-dialog.vue

@@ -0,0 +1,149 @@
+<template>
+    <el-dialog
+        v-model="dialogVisible"
+        title="协商(留言)"
+        width="500px"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+    >
+        <div>
+            <div class="mb-2">说明:</div>
+            <el-input
+                v-model="form.description"
+                type="textarea"
+                :rows="5"
+                placeholder="请输入"
+            />
+
+            <div class="mt-4 mb-2">附件:</div>
+            <ImageUpload
+                v-model="fileUrls"
+                multiple
+                :limit="3"
+                @change="handleFileUrlsChange"
+            />
+        </div>
+        <template #footer>
+            <span class="dialog-footer">
+                <el-button @click="handleCancel">取消</el-button>
+                <el-button type="primary" @click="handleSubmit">确定</el-button>
+            </span>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+    import {
+        ref,
+        reactive,
+        defineEmits,
+        defineProps,
+        defineExpose,
+        watch
+    } from 'vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+    import ImageUpload from '@/components/ImageUpload/index.vue';
+
+    const props = defineProps({
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        id: {
+            type: [Number, String],
+            default: ''
+        }
+    });
+
+    const emit = defineEmits(['update:visible', 'submit', 'cancel']);
+
+    // 对话框可见性
+    const dialogVisible = ref(false);
+
+    // 监听visible属性变化
+    watch(
+        () => props.visible,
+        (val) => {
+            dialogVisible.value = val;
+            if (val) {
+                // 重置表单
+                resetForm();
+            }
+        }
+    );
+
+    // 监听dialogVisible变化
+    watch(dialogVisible, (val) => {
+        emit('update:visible', val);
+    });
+
+    // 表单数据
+    const form = reactive({
+        id: '',
+        description: '',
+        fileUrls: []
+    });
+
+    // 文件URL字符串,用于ImageUpload组件
+    const fileUrls = ref('');
+
+    // 重置表单
+    const resetForm = () => {
+        form.id = props.id;
+        form.description = '';
+        form.fileUrls = [];
+        fileUrls.value = '';
+    };
+
+    // 处理文件URL变更
+    const handleFileUrlsChange = (value) => {
+        if (value) {
+            try {
+                // ImageUpload组件返回的是JSON字符串,需要解析
+                form.fileUrls = JSON.parse(value);
+            } catch (e) {
+                // 单个文件的情况,直接作为字符串处理
+                form.fileUrls = [value];
+            }
+        } else {
+            form.fileUrls = [];
+        }
+    };
+
+    // 取消
+    const handleCancel = () => {
+        dialogVisible.value = false;
+        emit('cancel');
+    };
+
+    // 提交
+    const handleSubmit = () => {
+        if (!form.description.trim()) {
+            EleMessage.warning('请输入协商内容');
+            return;
+        }
+
+        // 准备提交数据
+        const submitData = {
+            id: form.id,
+            description: form.description,
+            fileUrls: form.fileUrls
+        };
+
+        // 发送提交事件
+        emit('submit', submitData);
+    };
+
+    // 对外暴露方法
+    defineExpose({
+        resetForm
+    });
+</script>
+
+<style lang="scss" scoped>
+    .upload-demo {
+        .el-upload {
+            margin-right: 10px;
+        }
+    }
+</style>

+ 381 - 0
src/views/mallOrder/complaint/components/page-edit.vue

@@ -0,0 +1,381 @@
+<!-- 编辑弹窗 -->
+<template>
+    <ele-modal
+        form
+        :width="960"
+        v-model="visible"
+        title="投诉状态及操作"
+        @open="handleOpen"
+    >
+        <div class="flex w-full">
+            <div style="flex: 1.5">
+                <el-tag size="large">{{ getStatusText(form.status) }}</el-tag>
+
+                <ele-text style="margin-top: 15px"
+                    >1.请点击协商(留言)与对方协商。</ele-text
+                >
+                <ele-text>2.也可点击完结,投诉完结。</ele-text>
+                <el-divider />
+                <el-button @click="handleNegotiate">协商(留言)</el-button>
+                <el-button type="danger" @click="handleComplete"
+                    >完结</el-button
+                >
+                <el-divider />
+
+                <div class="common-title">协商历史</div>
+                <complain-item
+                    v-for="(item, index) in disposeLogList"
+                    :key="index"
+                    :item="item"
+                />
+            </div>
+            <div class="flex-1 complain-detail">
+                <div class="common-title">投诉详情</div>
+                <div class="detail-content mt-6">
+                    <div class="detail-item">
+                        <span class="label">投诉编号:</span>
+                        <span class="value">{{ form.id }}</span>
+                    </div>
+                    <div class="detail-item">
+                        <span class="label">投诉原因:</span>
+                        <span class="value">{{ form.reason }}</span>
+                    </div>
+                    <div class="detail-item">
+                        <span class="label">投诉说明:</span>
+                        <span class="value">{{ form.description }}</span>
+                    </div>
+                    <div class="detail-item">
+                        <span class="label">投诉时间:</span>
+                        <span class="value">{{ form.createTime }}</span>
+                    </div>
+                    <div class="detail-item">
+                        <span class="label">发 起 人:</span>
+                        <span class="value">{{ form.userName }}</span>
+                    </div>
+                    <div class="detail-item">
+                        <span class="label">订单编号:</span>
+                        <el-tooltip
+                            content="跳转到订单详情<br/>商品最多展示三个"
+                            placement="top"
+                            effect="light"
+                            raw-content
+                        >
+                            <span class="value link" @click="goToOrder(form.orderId)">{{ form.orderId }}</span>
+                        </el-tooltip>
+                    </div>
+                </div>
+
+                <el-divider />
+
+                <!-- 商品列表 -->
+                <div class="product-list">
+                    <div v-for="(item, index) in goodsList" :key="index" class="product-item">
+                        <el-image :src="item.image" class="product-img" fit="cover" />
+                        <div class="product-info">
+                            <div class="product-title">{{ item.title }}</div>
+                            <div class="product-meta">
+                                <span class="product-spec">{{ item.spec }}</span>
+                                <span class="product-qty" :class="{ 'text-red': item.quantity > 1 }">X{{ item.quantity }}</span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <template #footer>
+            <el-button @click="handleCancel">关闭</el-button>
+        </template>
+    </ele-modal>
+
+    <!-- 协商弹窗 -->
+    <negotiate-dialog
+        v-model:visible="negotiateVisible"
+        :id="currentId"
+        @submit="submitNegotiate"
+    />
+</template>
+
+<script setup>
+    import { ref, reactive, nextTick, getCurrentInstance } from 'vue';
+    import complainItem from './complain-item.vue';
+    import negotiateDialog from './negotiate-dialog.vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+    import { ElMessageBox } from 'element-plus';
+    import { useDictData } from '@/utils/use-dict-data';
+    import Logo from '@/assets/logo.png';
+
+    const { proxy } = getCurrentInstance();
+    const emit = defineEmits(['refresh']);
+
+    // 获取投诉状态字典
+    const [statusDicts] = useDictData(['complain_status']);
+
+    // 获取状态文本
+    const getStatusText = (status) => {
+        return (
+            statusDicts.value.find((d) => d.dictValue == status)?.dictLabel ||
+            '协商中'
+        );
+    };
+
+    /** 弹窗是否打开 */
+    const visible = defineModel({ type: Boolean });
+
+    /** 关闭弹窗 */
+    const handleCancel = () => {
+        visible.value = false;
+    };
+
+    // 当前投诉ID
+    const currentId = ref(null);
+    // 处理记录列表
+    const disposeLogList = ref([]);
+    // 商品列表
+    const goodsList = ref([]);
+
+    /** 弹窗打开事件 */
+    const handleOpen = (row) => {
+        if (row && row.id) {
+            currentId.value = row.id;
+            visible.value = true;
+            getComplaintDetail(row.id);
+        } else {
+            visible.value = true;
+            nextTick(() => console.log('打开'));
+        }
+    };
+
+    // 获取投诉详情
+    const getComplaintDetail = (id) => {
+        proxy.$http
+            .get(`/order/orderComplaintsLog/getInfo/${id}`)
+            .then((res) => {
+                if (res.data.code === 200) {
+                    const data = res.data.data || {};
+                    // 映射API返回的数据到表单
+                    form.id = data.id || '';
+                    form.userName = data.userName || '-';
+                    form.orderId = data.orderId || '';
+                    form.reason = data.reason || '';
+                    form.description = data.description || '';
+                    form.createTime = data.createTime || '';
+                    form.status = data.status || '';
+
+                    // 处理协商历史
+                    disposeLogList.value = data.disposeLogList || [];
+                    
+                    // 模拟商品数据 (因为后端可能没返回)
+                    goodsList.value = [
+                        {
+                            image: 'https://img14.360buyimg.com/n0/jfs/t1/157997/3/36676/122176/65e975a0F98822998/5023348143493720.jpg',
+                            title: '新版中日交流标准日本语初级上下册第二版日语零基础入门',
+                            spec: '品相 (一般)',
+                            quantity: 1
+                        },
+                        {
+                            image: 'https://img14.360buyimg.com/n0/jfs/t1/157997/3/36676/122176/65e975a0F98822998/5023348143493720.jpg',
+                            title: '新版中日交流标准日本语初级上下册第二版日语零基础入门',
+                            spec: '品相 (一般)',
+                            quantity: 2
+                        },
+                         {
+                            image: 'https://img14.360buyimg.com/n0/jfs/t1/157997/3/36676/122176/65e975a0F98822998/5023348143493720.jpg',
+                            title: '新版中日交流标准日本语初级上下册第二版日语零基础入门',
+                            spec: '品相 (一般)',
+                            quantity: 1
+                        }
+                    ];
+                } else {
+                    EleMessage.error(res.data.msg || '获取投诉详情失败');
+                }
+            })
+            .catch((err) => {
+                console.error('获取投诉详情失败', err);
+                EleMessage.error('获取投诉详情失败');
+            });
+    };
+
+    // 协商弹窗相关
+    const negotiateVisible = ref(false);
+
+    // 处理协商
+    const handleNegotiate = () => {
+        if (!currentId.value) {
+            EleMessage.warning('请先选择投诉记录');
+            return;
+        }
+
+        // 打开协商弹窗
+        negotiateVisible.value = true;
+    };
+
+    // 提交协商
+    const submitNegotiate = (formData) => {
+        // 调用协商API
+        proxy.$http
+            .post('/order/orderComplaintsLog/dispose', formData)
+            .then((res) => {
+                if (res.data.code === 200) {
+                    EleMessage.success('协商成功');
+                    negotiateVisible.value = false;
+
+                    // 刷新详情,更新协商历史
+                    getComplaintDetail(currentId.value);
+                } else {
+                    EleMessage.error(res.data.msg || '协商失败');
+                }
+            })
+            .catch((err) => {
+                console.error('协商失败', err);
+                EleMessage.error('协商失败');
+            });
+    };
+
+    // 处理完结
+    const handleComplete = () => {
+        if (!currentId.value) {
+            EleMessage.warning('请先选择投诉记录');
+            return;
+        }
+
+        ElMessageBox.confirm('确认完结此投诉?', '提示', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+        })
+            .then(() => {
+                // 调用完结API
+                proxy.$http
+                    .post(`/order/orderComplaintsLog/over/${currentId.value}`)
+                    .then((res) => {
+                        if (res.data.code === 200) {
+                            EleMessage.success('投诉已完结');
+                            emit('refresh'); // 通知父组件刷新列表
+                            handleCancel(); // 关闭弹窗
+                        } else {
+                            EleMessage.error(res.data.msg || '操作失败');
+                        }
+                    })
+                    .catch((err) => {
+                        console.error('完结投诉失败', err);
+                        EleMessage.error('完结投诉失败');
+                    });
+            })
+            .catch(() => {
+                // 用户取消操作
+            });
+    };
+    
+    // 跳转到订单详情
+    const goToOrder = (orderId) => {
+        if (!orderId) return;
+        // 由于暂无订单详情页,暂时提示
+        EleMessage.info(`功能开发中,订单号: ${orderId}`);
+        // 实际开发中应该是: router.push({ path: '/mallOrder/detail', query: { id: orderId } })
+    };
+
+    const form = reactive({
+        id: '',
+        userName: '',
+        orderId: '',
+        reason: '',
+        description: '',
+        createTime: '',
+        status: ''
+    });
+
+    defineExpose({
+        handleOpen
+    });
+</script>
+
+<style lang="scss">
+    .complain-detail {
+        border-left: 1px solid #e6e6e6;
+        padding-left: 15px;
+        
+        .detail-item {
+            margin-bottom: 15px;
+            font-size: 14px;
+            display: flex;
+            
+            .label {
+                color: #666;
+                width: 80px;
+                flex-shrink: 0;
+            }
+            
+            .value {
+                color: #333;
+                flex: 1;
+                
+                &.link {
+                    color: #409eff;
+                    cursor: pointer;
+                    &:hover {
+                        text-decoration: underline;
+                    }
+                }
+            }
+        }
+        
+        .product-list {
+            .product-item {
+                display: flex;
+                margin-bottom: 15px;
+                padding-bottom: 15px;
+                border-bottom: 1px solid #f5f5f5;
+                
+                &:last-child {
+                    border-bottom: none;
+                }
+                
+                .product-img {
+                    width: 80px;
+                    height: 80px;
+                    border-radius: 4px;
+                    margin-right: 10px;
+                    flex-shrink: 0;
+                    background-color: #f5f5f5;
+                }
+                
+                .product-info {
+                    flex: 1;
+                    display: flex;
+                    flex-direction: column;
+                    justify-content: space-between;
+                    
+                    .product-title {
+                        font-weight: bold;
+                        color: #333;
+                        font-size: 14px;
+                        line-height: 1.4;
+                        margin-bottom: 5px;
+                    }
+                    
+                    .product-meta {
+                        display: flex;
+                        justify-content: space-between;
+                        align-items: flex-end;
+                        
+                        .product-spec {
+                            color: #999;
+                            font-size: 12px;
+                        }
+                        
+                        .product-qty {
+                            font-size: 14px;
+                            font-weight: bold;
+                            color: #333;
+                            
+                            &.text-red {
+                                color: #ff4d4f;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 69 - 0
src/views/mallOrder/complaint/components/page-search.vue

@@ -0,0 +1,69 @@
+<!-- 搜索表单 -->
+<template>
+    <ele-card :body-style="{ paddingBottom: '8px' }">
+        <ProSearch
+            :items="formItems"
+            ref="searchRef"
+            @search="search"
+            :initKeys="initKeys"
+        />
+    </ele-card>
+</template>
+
+<script setup>
+    import { reactive, ref, defineEmits } from 'vue';
+    import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+    const emit = defineEmits(['search']);
+
+    const formItems = reactive([
+        { type: 'input', label: '订单编号', prop: 'orderId' },
+        { type: 'input', label: '投诉编号', prop: 'id' },
+        {
+            type: 'datetimerange',
+            label: '投诉时间',
+            prop: 'time',
+            props: {
+                format: 'YYYY-MM-DD HH:mm:ss',
+                valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                onChange: (value) => {
+                    searchRef.value?.setData({
+                        createTimeStart: value ? value[0] : '',
+                        createTimeEnd: value ? value[1] : ''
+                    });
+                }
+            },
+            colProps: { span: 6 }
+        },
+        {
+            type: 'dictSelect',
+            label: '投诉原因',
+            prop: 'reason',
+            props: { code: 'optimization_content' },
+            colProps: { span: 3 }
+        },
+        {
+            type: 'dictSelect',
+            label: '投诉状态',
+            prop: 'status',
+            props: { code: 'complain_status' },
+            colProps: { span: 3 }
+        }
+    ]);
+
+    const initKeys = reactive({
+        id: '',
+        orderId: '',
+        reason: '',
+        time: [],
+        createTimeStart: '',
+        createTimeEnd: '',
+        status: ''
+    });
+
+    const searchRef = ref(null);
+    /** 搜索 */
+    const search = (data) => {
+        emit('search', { ...data });
+    };
+</script>

+ 125 - 0
src/views/mallOrder/complaint/index.vue

@@ -0,0 +1,125 @@
+<template>
+    <ele-page flex-table>
+        <page-search @search="reload" />
+        <common-table
+            ref="pageRef"
+            :pageConfig="pageConfig"
+            :columns="columns"
+            :tools="false"
+        >
+            <template #baseinfo="{ row }">
+                <div class="flex flex-col items-start">
+                    <el-tag size="large" class="w-full">
+                        <el-text class="flex-1">投诉编号:{{ row.id }}</el-text>
+                        <el-text class="flex-1"
+                            >订单编号:{{ row.orderId }}</el-text
+                        >
+                    </el-tag>
+                    <div class="demo-image__preview mt-2">
+                        <el-image
+                            v-for="item in row.imgList"
+                            :key="item"
+                            style="
+                                width: 70px;
+                                height: 70px;
+                                border-radius: 5px;
+                                margin-right: 8px;
+                            "
+                            :src="item"
+                            fit="cover"
+                            :preview-src-list="row.imgList"
+                            :initial-index="row.imgList.indexOf(item)"
+                            preview-teleported
+                        />
+                    </div>
+                </div>
+            </template>
+            <template #action="{ row }">
+                <div>
+                    <el-button
+                        type="primary"
+                        link
+                        v-permission="'optimization:complain:detail'"
+                        @click="handleDetail(row)"
+                    >
+                        详情
+                    </el-button>
+                </div>
+            </template>
+        </common-table>
+        <page-edit ref="pageEditRef" @refresh="reload" />
+    </ele-page>
+</template>
+
+<script setup>
+    import { ref, reactive } from 'vue';
+    import CommonTable from '@/components/CommonPage/CommonTable.vue';
+    import pageSearch from './components/page-search.vue';
+    import pageEdit from './components/page-edit.vue';
+    import { useDictData } from '@/utils/use-dict-data';
+
+    defineOptions({ name: 'MallOrderComplaintList' });
+    const [statusDicts] = useDictData(['complain_status']);
+
+    /** 表格列配置 */
+    const columns = ref([
+        {
+            label: '基础信息',
+            prop: 'baseinfo',
+            align: 'center',
+            minWidth: 240,
+            slot: 'baseinfo'
+        },
+        {
+            label: '投诉人',
+            prop: 'userId',
+            align: 'center'
+        },
+        {
+            label: '投诉时间',
+            prop: 'createTime',
+            align: 'center',
+            width: 180
+        },
+        {
+            label: '投诉原因',
+            prop: 'reason',
+            align: 'center'
+        },
+        {
+            label: '投诉状态',
+            prop: 'status',
+            align: 'center',
+            formatter: (row) =>
+                statusDicts.value.find((d) => d.dictValue == row.status)
+                    ?.dictLabel
+        },
+        {
+            columnKey: 'action',
+            label: '操作',
+            width: 120,
+            align: 'center',
+            slot: 'action'
+        }
+    ]);
+
+    /** 页面组件实例 */
+    const pageRef = ref(null);
+
+    const pageConfig = reactive({
+        pageUrl: '/order/orderComplaintsLog/pagelist',
+        fileName: '投诉管理',
+        cacheKey: 'mallOrderComplainTable'
+    });
+
+    //刷新表格
+    function reload(where) {
+        pageRef.value?.reload(where);
+    }
+
+    //处理
+    const pageEditRef = ref(null);
+    function handleDetail(row) {
+        pageEditRef.value?.handleOpen(row);
+    }
+</script>

+ 0 - 0
src/views/mallOrder/refund/index.vue


+ 207 - 260
src/views/recycleLogistics/warehouse/components/area-setting.vue

@@ -1,106 +1,51 @@
 <!-- 编辑弹窗 -->
 <template>
-    <ele-modal
-        form
-        :width="1080"
-        :bodyStyle="{ 'min-height': '400px' }"
-        v-model="visible"
-        :title="title"
-        @open="handleOpen"
-    >
+    <ele-modal form :width="1080" :bodyStyle="{ 'min-height': '400px' }" v-model="visible" :title="title"
+        @open="handleOpen">
         <div class="flex flex-col" v-loading="loading">
             <div class="flex" v-for="dq in areaList" :key="dq.code">
-                <el-checkbox
-                    :label="dq.name"
-                    :indeterminate="isIndeterminateShow(dq)"
-                    style="min-width: 100px"
-                    @change="(value) => handleDqChange(value, dq)"
-                />
-
-                <div v-for="item in dq.children" class="mr-6 flex items-center">
-                    <el-checkbox
-                        @change="(value) => handleParentChange(value, item)"
-                        :data-id="item.id"
-                        v-model="item.otherSelected"
-                        style="margin-right: 5px"
-                        :true-value="1"
-                        :false-value="0"
-                        :indeterminate="
-                            isIndeterCityShow(item, 'otherSelected')
-                        "
-                        v-if="item.otherSelected == 1"
-                        disabled
-                    />
-                    <el-checkbox
-                        @change="(value) => handleParentChange(value, item)"
-                        :data-id="item.id"
-                        v-model="item.mySelected"
-                        style="margin-right: 5px"
-                        :true-value="1"
-                        :false-value="0"
-                        :indeterminate="isIndeterCityShow(item, 'mySelected')"
-                        v-else
-                    />
-                    <ele-popover
-                        :width="400"
-                        trigger="click"
-                        :title="item.district"
-                        :showArrow="false"
-                        :disabled="isPopoverDisabled(item)"
-                    >
+                <el-checkbox :label="dq.name" :indeterminate="isIndeterminateShow(dq)" style="min-width: 100px"
+                    @change="(value) => handleDqChange(value, dq)" />
+
+                <div v-for="item in dq.children" class="mr-6 flex items-center" :key="item.code">
+                    <el-checkbox @change="(value) => handleParentChange(value, item)" :data-id="item.id"
+                        v-model="item.otherSelected" style="margin-right: 5px" :true-value="1" :false-value="0"
+                        :indeterminate="isIndeterCityShow(item, 'otherSelected')
+                            " v-if="item.otherSelected == 1" disabled />
+                    <el-checkbox @change="(value) => handleParentChange(value, item)" :data-id="item.id"
+                        v-model="item.mySelected" style="margin-right: 5px" :true-value="1" :false-value="0"
+                        :indeterminate="isIndeterCityShow(item, 'mySelected')" v-else />
+                    <ele-popover :width="400" trigger="click" :title="item.district" :showArrow="false"
+                        :disabled="isPopoverDisabled(item)">
                         <template #reference>
-                            <div
-                                class="flex items-center cursor-pointer"
-                                :class="{
-                                    'disabled-popover': isPopoverDisabled(item)
-                                }"
-                            >
-                                <el-text
-                                    :type="
-                                        item.mySelected == 1 ? 'primary' : ''
-                                    "
-                                    >{{ item.district }}</el-text
-                                >
-                                <el-text type="warning" v-if="isShowAll(item)"
-                                    >(全)</el-text
-                                >
-                                <el-icon><CaretBottom /></el-icon>
+                            <div class="flex items-center cursor-pointer" :class="{
+                                'disabled-popover': isPopoverDisabled(item)
+                            }">
+                                <el-text :type="item.mySelected == 1 ? 'primary' : ''
+                                    ">{{ item.district }}</el-text>
+                                <el-text type="warning" v-if="isShowAll(item)">(全)</el-text>
+                                <el-icon>
+                                    <CaretBottom />
+                                </el-icon>
                             </div>
                         </template>
                         <template v-for="child in item.childInfo">
-                            <el-checkbox
-                                @change="
-                                    (value) =>
-                                        handleChildChange(value, item, child)
-                                "
-                                :data-id="child.id"
-                                :label="child.district"
-                                v-model="child.otherSelected"
-                                :true-value="1"
-                                :false-value="0"
-                                v-if="child.otherSelected == 1"
-                                disabled
-                            />
-                            <el-checkbox
-                                v-else
-                                @change="
-                                    (value) =>
-                                        handleChildChange(value, item, child)
-                                "
-                                :data-id="child.id"
-                                v-model="child.mySelected"
-                                :label="child.district"
-                                :true-value="1"
-                                :false-value="0"
-                            />
+                            <el-checkbox @change="
+                                (value) =>
+                                    handleChildChange(value, item, child)
+                            " :data-id="child.id" :label="child.district" v-model="child.otherSelected" :true-value="1"
+                                :false-value="0" v-if="child.otherSelected == 1" disabled />
+                            <el-checkbox v-else @change="
+                                (value) =>
+                                    handleChildChange(value, item, child)
+                            " :data-id="child.id" v-model="child.mySelected" :label="child.district" :true-value="1"
+                                :false-value="0" />
                         </template>
                     </ele-popover>
                 </div>
             </div>
         </div>
-        <el-text type="danger"
-            >灰色地区表示已经添加到其他仓库中,请先在其他仓库中去掉勾选后,再重新选择</el-text
-        >
+        <el-text type="danger">灰色地区表示已经添加到其他仓库中,请先在其他仓库中去掉勾选后,再重新选择</el-text>
 
         <template #footer>
             <el-button @click="handleCancel">关闭</el-button>
@@ -110,188 +55,190 @@
 </template>
 
 <script setup>
-    import { ref, reactive, nextTick } from 'vue';
-    import request from '@/utils/request';
-    import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
-
-    /** 弹窗是否打开 */
-    const visible = defineModel({ type: Boolean });
-
-    /** 关闭弹窗 */
-    const handleCancel = () => {
-        visible.value = false;
-    };
-
-    //计算是否禁用
-    const isPopoverDisabled = computed(() => (item) => {
-        let checked = item.childInfo.every((i) => i.otherSelected == 1);
-        return checked;
-    });
-
-    //计算是否显示全
-    const isShowAll = computed(() => (item) => {
-        let checked = item.childInfo.every((i) => i.mySelected == 1);
-        return checked;
-    });
-    //计算大区是否全选
-    const isIndeterminateShow = computed(() => (dq) => {
-        let len = dq.children.length;
-        let length = dq.children.filter((i) => i.mySelected == 1).length;
-
-        return length > 0 && length < len;
-    });
-
-    //计算省市是否全选
-    const isIndeterCityShow = computed(() => (item, type) => {
-        let len = item.childInfo.length;
-        let length = item.childInfo.filter((i) => i[type] == 1).length;
-
-        return length > 0 && length < len;
+import { ref, reactive, nextTick } from 'vue';
+import request from '@/utils/request';
+import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
+
+/** 弹窗是否打开 */
+const visible = defineModel({ type: Boolean });
+
+/** 关闭弹窗 */
+const handleCancel = () => {
+    visible.value = false;
+};
+
+//计算是否禁用
+const isPopoverDisabled = computed(() => (item) => {
+    let checked = item.childInfo.every((i) => i.otherSelected == 1);
+    return checked;
+});
+
+//计算是否显示全
+const isShowAll = computed(() => (item) => {
+    let checked = item.childInfo.every((i) => i.mySelected == 1);
+    return checked;
+});
+//计算大区是否全选
+const isIndeterminateShow = computed(() => (dq) => {
+    let len = dq.children.length;
+    let length = dq.children.filter((i) => i.mySelected == 1).length;
+
+    return length > 0 && length < len;
+});
+
+//计算省市是否全选
+const isIndeterCityShow = computed(() => (item, type) => {
+    let len = item.childInfo.length;
+    let length = item.childInfo.filter((i) => i[type] == 1).length;
+
+    return length > 0 && length < len;
+});
+
+/** 弹窗打开事件 */
+const title = ref('仓库区域设置');
+const handleOpen = (row) => {
+    visible.value = true;
+    nextTick(() => {
+        if (row && row.id) {
+            title.value = row.godownName
+                ? `${row.godownName}区域设置`
+                : `仓库区域设置`;
+
+            row && getAreaInfo(row.id);
+        }
     });
+};
+
+let formdata = reactive({
+    godownId: '',
+    selectendList: [{ provinceId: '', cityId: '' }]
+});
+
+//获取区域基础数据信息
+let dqList = ref([
+    { name: '华东', code: 'hd' },
+    { name: '华北', code: 'hb' },
+    { name: '华中', code: 'hz' },
+    { name: '华南', code: 'hn' },
+    { name: '东北', code: 'db' },
+    { name: '西北', code: 'xb' },
+    { name: '西南', code: 'xn' },
+    { name: '港澳台', code: 'gat' },
+    { name: '海外', code: 'hw' }
+]);
+const areaList = ref([]);
+const loading = ref(false);
+const getAreaInfo = (godownId) => {
+    formdata.godownId = godownId;
+    loading.value = true;
+    request
+        .get(`/baseinfo/godown/getGodownAreaList/${godownId}`)
+        .then((res) => {
+            if (res.data.code == 200) {
+                areaList.value.length = 0;
+                dqList.value.forEach((item) => {
+                    item.checked = false;
+                    item.children = res.data.data[item.code];
+                    areaList.value.push(item);
+                });
 
-    /** 弹窗打开事件 */
-    const title = ref('仓库区域设置');
-    const handleOpen = (row) => {
-        visible.value = true;
-        nextTick(() => {
-            if (row && row.id) {
-                title.value = row.godownName
-                    ? `${row.godownName}区域设置`
-                    : `仓库区域设置`;
-
-                row && getAreaInfo(row.id);
+                console.log(areaList.value, 'areaList.value');
             }
-        });
-    };
-
-    let formdata = reactive({
-        godownId: '',
-        selectendList: [{ provinceId: '', cityId: '' }]
-    });
-
-    //获取区域基础数据信息
-    let dqList = ref([
-        { name: '华东', code: 'hd' },
-        { name: '华北', code: 'hb' },
-        { name: '华中', code: 'hz' },
-        { name: '华南', code: 'hn' },
-        { name: '东北', code: 'db' },
-        { name: '西北', code: 'xb' },
-        { name: '西南', code: 'xn' },
-        { name: '港澳台', code: 'gat' },
-        { name: '海外', code: 'hw' }
-    ]);
-    const areaList = ref([]);
-    const loading = ref(false);
-    const getAreaInfo = (godownId) => {
-        formdata.godownId = godownId;
-        loading.value = true;
-        request
-            .get(`/baseinfo/godown/getGodownAreaList/${godownId}`)
-            .then((res) => {
-                if (res.data.code == 200) {
-                    areaList.value.length = 0;
-                    dqList.value.forEach((item) => {
-                        item.checked = false;
-                        item.children = res.data.data[item.code];
-                        areaList.value.push(item);
-                    });
-
-                    console.log(areaList.value, 'areaList.value');
-                }
-            })
-            .finally(() => (loading.value = false));
-    };
-
-    //区域checkbox改变事件
-    const handleDqChange = (value, dq) => {
-        if (value == 1) {
-            dq.children.forEach((item) => {
-                if (item.otherSelected != 1) {
-                    item.mySelected = 1;
-                    item.childInfo.forEach((i) => {
-                        if (i.otherSelected != 1) {
-                            i.mySelected = 1;
-                        }
-                    });
-                }
-            });
-        } else if (value == 0) {
-            dq.children.forEach((item) => {
-                if (item.otherSelected != 1) {
-                    item.mySelected = 0;
-                    item.childInfo.forEach((i) => {
-                        if (i.otherSelected != 1) {
-                            i.mySelected = 1;
-                        }
-                    });
-                }
-            });
-        }
-    };
-
-    //父级checkbox改变事件
-    const handleParentChange = (value, item) => {
-        if (value == 1) {
-            item.childInfo.forEach((i) => {
-                i.mySelected = 1;
-            });
-        } else if (value == 0) {
-            item.childInfo.forEach((i) => {
-                i.mySelected = 0;
-            });
-        }
-    };
-    //子级checkbox改变事件
-    const handleChildChange = (value, item, child) => {
-        let checked = item.childInfo.some((i) => i.mySelected == 1);
-        item.mySelected = checked ? 1 : 0;
-    };
-
-    //格式化选中数据
-    const formatterSelected = () => {
-        let arr = [];
-        areaList.value.forEach((item) => {
-            item.children.forEach((i) => {
-                i.childInfo.forEach((j) => {
-                    if (j.mySelected == 1 && j.otherSelected != 1) {
-                        arr.push({
-                            provinceId: i.id,
-                            cityId: j.id
-                        });
+        })
+        .finally(() => (loading.value = false));
+};
+
+//区域checkbox改变事件
+const handleDqChange = (value, dq) => {
+    if (value == 1) {
+        dq.children.forEach((item) => {
+            if (item.otherSelected != 1) {
+                item.mySelected = 1;
+                item.childInfo.forEach((i) => {
+                    if (i.otherSelected != 1) {
+                        i.mySelected = 1;
                     }
                 });
-            });
+            }
         });
-        return arr;
-    };
-
-    /** 提交 */
-    const handleSubmit = () => {
-        formdata.selectendList = formatterSelected();
-        console.log(formdata);
-        let data = JSON.parse(JSON.stringify(formdata));
-        request.post(`/baseinfo/godown/setGodownAreaInfo`, data).then((res) => {
-            if (res.data.code == 200) {
-                visible.value = false;
+    } else if (value == 0) {
+        dq.children.forEach((item) => {
+            if (item.otherSelected != 1) {
+                item.mySelected = 0;
+                item.childInfo.forEach((i) => {
+                    if (i.otherSelected != 1) {
+                        i.mySelected = 1;
+                    }
+                });
             }
         });
-    };
+    }
+};
 
-    defineExpose({
-        handleOpen
+//父级checkbox改变事件
+const handleParentChange = (value, item) => {
+    if (value == 1) {
+        item.childInfo.forEach((i) => {
+            i.mySelected = 1;
+        });
+    } else if (value == 0) {
+        item.childInfo.forEach((i) => {
+            i.mySelected = 0;
+        });
+    }
+};
+//子级checkbox改变事件
+const handleChildChange = (value, item, child) => {
+    let checked = item.childInfo.some((i) => i.mySelected == 1);
+    item.mySelected = checked ? 1 : 0;
+};
+
+//格式化选中数据
+const formatterSelected = () => {
+    let arr = [];
+    areaList.value.forEach((item) => {
+        item.children.forEach((i) => {
+            i.childInfo.forEach((j) => {
+                if (j.mySelected == 1 && j.otherSelected != 1) {
+                    arr.push({
+                        provinceId: i.id,
+                        cityId: j.id
+                    });
+                }
+            });
+        });
+    });
+    return arr;
+};
+
+/** 提交 */
+const handleSubmit = () => {
+    formdata.selectendList = formatterSelected();
+    console.log(formdata);
+    let data = JSON.parse(JSON.stringify(formdata));
+    request.post(`/baseinfo/godown/setGodownAreaInfo`, data).then((res) => {
+        if (res.data.code == 200) {
+            visible.value = false;
+        }
     });
+};
+
+defineExpose({
+    handleOpen
+});
 </script>
 
 <style lang="scss">
-    .disabled-popover {
-        cursor: not-allowed;
-        .el-text {
-            color: #909399;
-        }
-        .el-icon {
-            color: #909399;
-            fill: #909399;
-        }
+.disabled-popover {
+    cursor: not-allowed;
+
+    .el-text {
+        color: #909399;
+    }
+
+    .el-icon {
+        color: #909399;
+        fill: #909399;
     }
+}
 </style>

+ 172 - 0
src/views/statsAnalysis/marketing/components/ActivityAnalysis.vue

@@ -0,0 +1,172 @@
+<template>
+	<div class="activity-analysis">
+		<div class="section-title">{{ title }}</div>
+
+		<!-- Stats Grid -->
+		<div class="stats-grid">
+			<div v-for="(stat, index) in stats" :key="index" class="stat-card">
+				<div class="stat-label">{{ stat.label }}</div>
+				<div class="stat-value">{{ stat.value }}</div>
+			</div>
+		</div>
+
+		<!-- Charts Row -->
+		<div class="charts-row">
+			<!-- Left: New/Old Repurchase -->
+			<div class="chart-box left-chart">
+				<div class="chart-title">拉新复购</div>
+				<div class="chart-content">
+					<base-chart :option="donutOption" />
+				</div>
+			</div>
+
+			<!-- Right: Traffic Conversion -->
+			<div class="chart-box right-chart">
+				<div class="chart-title">流量转化</div>
+				<div class="chart-content">
+					<funnel-chart :visitor-count="funnelData.visitorCount" :payment-count="funnelData.paymentCount"
+						:unit-price="funnelData.unitPrice" :conversion-rate="funnelData.conversionRate" />
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import BaseChart from '@/components/Chart/BaseChart.vue';
+import FunnelChart from '@/components/Chart/FunnelChart.vue';
+
+const props = defineProps({
+	title: {
+		type: String,
+		required: true
+	},
+	stats: {
+		type: Array,
+		default: () => []
+	},
+	newCustomerRatio: {
+		type: Number,
+		default: 0.6 // 60% new
+	},
+	funnelData: {
+		type: Object,
+		default: () => ({
+			visitorCount: 32,
+			paymentCount: 32,
+			unitPrice: 32,
+			conversionRate: '65%'
+		})
+	}
+});
+
+const donutOption = computed(() => ({
+	tooltip: {
+		trigger: 'item'
+	},
+	legend: {
+		top: '5%',
+		left: 'center',
+		data: ['老客户成交占比', '新客户成交占比']
+	},
+	series: [
+		{
+			name: '成交占比',
+			type: 'pie',
+			radius: ['40%', '70%'],
+			avoidLabelOverlap: false,
+			itemStyle: {
+				borderRadius: 0,
+				borderColor: '#fff',
+				borderWidth: 2
+			},
+			label: {
+				show: true,
+				formatter: '{b}\n{d}%'
+			},
+			emphasis: {
+				label: {
+					show: true,
+					fontSize: 16,
+					fontWeight: 'bold'
+				}
+			},
+			labelLine: {
+				show: true
+			},
+			data: [
+				{ value: 1 - props.newCustomerRatio, name: '老客户成交占比', itemStyle: { color: '#5b8ff9' } },
+				{ value: props.newCustomerRatio, name: '新客户成交占比', itemStyle: { color: '#5ebcb9' } }
+			]
+		}
+	]
+}));
+</script>
+
+<style scoped lang="scss">
+.activity-analysis {
+	margin-top: 30px;
+	border-top: 1px solid #eee;
+	padding-top: 20px;
+}
+
+.section-title {
+	font-size: 16px;
+	font-weight: bold;
+	margin-bottom: 20px;
+	color: #333;
+}
+
+.stats-grid {
+	display: grid;
+	grid-template-columns: repeat(4, 1fr);
+	gap: 15px;
+	margin-bottom: 20px;
+}
+
+.stat-card {
+	background-color: #f0f2f5;
+	padding: 20px;
+	border-radius: 4px;
+	text-align: center;
+
+	.stat-label {
+		color: #666;
+		font-size: 14px;
+	}
+
+	.stat-value {
+		color: #333;
+		font-size: 26px;
+		font-weight: 500;
+	}
+}
+
+.charts-row {
+	display: flex;
+	gap: 20px;
+}
+
+.chart-box {
+	flex: 1;
+	background-color: #f0f2f5;
+	padding: 20px;
+	border-radius: 4px;
+	height: 350px;
+	display: flex;
+	flex-direction: column;
+
+	.chart-title {
+		font-size: 14px;
+		font-weight: bold;
+		color: #666;
+		margin-bottom: 10px;
+	}
+
+	.chart-content {
+		flex: 1;
+		position: relative;
+	}
+}
+</style>

+ 218 - 0
src/views/statsAnalysis/marketing/components/MarketingOverview.vue

@@ -0,0 +1,218 @@
+<template>
+	<div class="marketing-overview">
+		<div class="overview-section">
+			<div class="section-title">营销概要</div>
+
+			<!-- Top Section: Analysis -->
+			<ele-card class="analysis-card" :body-style="{ padding: '20px' }">
+				<div class="card-header">营销分析</div>
+
+				<div class="stats-row">
+					<div class="stat-item">
+						<div class="stat-label">有效活动数</div>
+						<div class="stat-value">3</div>
+					</div>
+					<div class="stat-item">
+						<div class="stat-label">营销支付金额</div>
+						<div class="stat-value">366994.6</div>
+					</div>
+					<div class="stat-item">
+						<div class="stat-label">营销支付金额占比</div>
+						<div class="stat-value">15.6%</div>
+					</div>
+				</div>
+
+				<div class="chart-wrapper horizontal-bar">
+					<base-chart :option="marketingDistributionOption" />
+				</div>
+			</ele-card>
+
+			<!-- Bottom Section: Payment Amount -->
+			<ele-card class="payment-card" :body-style="{ padding: '20px' }">
+				<div class="card-header">支付金额</div>
+				<div class="chart-wrapper payment-bar">
+					<base-chart :option="paymentAmountOption" />
+				</div>
+			</ele-card>
+		</div>
+	</div>
+</template>
+
+<script setup>
+import { reactive } from 'vue';
+import BaseChart from '@/components/Chart/BaseChart.vue';
+
+// Marketing Distribution Option (Horizontal Stacked Bar)
+const marketingDistributionOption = reactive({
+	tooltip: {
+		trigger: 'axis',
+		axisPointer: { type: 'shadow' }
+	},
+	legend: {
+		bottom: 0,
+		data: ['惊喜红包', '分享降价', '满减优惠']
+	},
+	grid: {
+		left: '3%',
+		right: '4%',
+		bottom: '15%',
+		containLabel: true,
+		height: '60px', // Thin bar
+		top: 'center'
+	},
+	xAxis: {
+		type: 'value',
+		show: false,
+		splitLine: { show: false }
+	},
+	yAxis: {
+		type: 'category',
+		data: [''],
+		show: false,
+		splitLine: { show: false }
+	},
+	series: [
+		{
+			name: '惊喜红包',
+			type: 'bar',
+			stack: 'total',
+			label: { show: true, position: 'inside' },
+			emphasis: { focus: 'series' },
+			data: [100],
+			itemStyle: { color: '#d4ad5b' },
+			barWidth: 40
+		},
+		{
+			name: '分享降价',
+			type: 'bar',
+			stack: 'total',
+			label: { show: true, position: 'inside' },
+			emphasis: { focus: 'series' },
+			data: [300],
+			itemStyle: { color: '#5ebcb9' }
+		},
+		{
+			name: '满减优惠',
+			type: 'bar',
+			stack: 'total',
+			label: { show: true, position: 'inside' },
+			emphasis: { focus: 'series' },
+			data: [100],
+			itemStyle: { color: '#4caf50' }
+		}
+	]
+});
+
+// Payment Amount Option (Bar Chart)
+const paymentAmountOption = reactive({
+	tooltip: {
+		trigger: 'axis',
+		axisPointer: { type: 'shadow' }
+	},
+	legend: {
+		top: 0,
+		right: 20,
+		data: ['营销金额', '非营销金额']
+	},
+	grid: {
+		left: '3%',
+		right: '4%',
+		bottom: '3%',
+		containLabel: true
+	},
+	xAxis: {
+		type: 'category',
+		data: ['8.1', '8.2', '8.3', '8.4', '8.5', '8.6', '8.7', '8.8', '8.9', '8.10', '8.11', '8.12']
+	},
+	yAxis: {
+		type: 'value'
+	},
+	series: [
+		{
+			name: '营销金额',
+			type: 'bar',
+			data: [100, 140, 230, 100, 130, 210, 200, 180, 115, 150, 220, 120],
+			itemStyle: { color: '#5b8ff9' }
+		},
+		{
+			name: '非营销金额',
+			type: 'bar',
+			data: [150, 100, 200, 140, 100, 230, 210, 150, 145, 210, 190, 130],
+			itemStyle: { color: '#5ebcb9' }
+		}
+	]
+});
+</script>
+
+<style scoped lang="scss">
+.marketing-overview {
+	margin-bottom: 20px;
+}
+
+.section-title {
+	font-size: 16px;
+	font-weight: bold;
+	margin-bottom: 15px;
+	color: #333;
+}
+
+.analysis-card {
+	margin-bottom: 20px;
+	background-color: #f5f5f5; // Light gray bg from design
+	border: none;
+
+	:deep(.el-card__body) {
+		background-color: #f0f2f5;
+	}
+}
+
+.payment-card {
+	background-color: #f5f5f5;
+	border: none;
+
+	:deep(.el-card__body) {
+		background-color: #f0f2f5;
+	}
+}
+
+.card-header {
+	font-size: 16px;
+	color: #333;
+	margin-bottom: 20px;
+}
+
+.stats-row {
+	display: flex;
+	justify-content: space-around;
+	margin-bottom: 30px;
+	text-align: center;
+
+	.stat-label {
+		color: #666;
+		font-size: 14px;
+		margin-bottom: 10px;
+	}
+
+	.stat-value {
+		color: #333;
+		font-size: 24px;
+		font-weight: 500;
+	}
+}
+
+.chart-wrapper {
+	width: 100%;
+
+	&.horizontal-bar {
+		height: 100px;
+
+		:deep(.base-chart) {
+			min-height: 100px;
+		}
+	}
+
+	&.payment-bar {
+		height: 350px;
+	}
+}
+</style>

+ 132 - 0
src/views/statsAnalysis/marketing/index.vue

@@ -0,0 +1,132 @@
+<template>
+    <ele-page>
+        <ele-card :body-style="{ padding: '20px' }">
+            <!-- Header / Filters -->
+            <div class="filter-container">
+                <div class="date-ranges">
+                    <el-button v-for="range in dateRanges" :key="range.value"
+                        :type="activeRange === range.value ? 'primary' : ''" @click="activeRange = range.value">
+                        {{ range.label }}
+                    </el-button>
+                </div>
+                <div class="date-picker-wrapper">
+                    <el-date-picker v-model="dateRange" type="daterange" range-separator="到" start-placeholder="开始时间"
+                        end-placeholder="结束时间" />
+                </div>
+                <el-button type="primary" @click="handleSearch">搜索</el-button>
+            </div>
+
+            <!-- Marketing Overview -->
+            <marketing-overview />
+
+            <!-- Surprise Red Packet Activity -->
+            <activity-analysis title="惊喜红包活动" :stats="redPacketStats" :new-customer-ratio="0.6"
+                :funnel-data="funnelData1" />
+
+            <!-- Share Price Reduction Activity -->
+            <activity-analysis title="分享降价活动" :stats="shareStats" :new-customer-ratio="0.35"
+                :funnel-data="funnelData2" />
+
+            <!-- Full Reduction Discount -->
+            <activity-analysis title="满减优惠" :stats="fullReductionStats" :new-customer-ratio="0.65"
+                :funnel-data="funnelData3" />
+
+        </ele-card>
+    </ele-page>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import MarketingOverview from './components/MarketingOverview.vue';
+import ActivityAnalysis from './components/ActivityAnalysis.vue';
+
+// Filters
+const activeRange = ref('yesterday');
+const dateRange = ref([]);
+const dateRanges = [
+    { label: '昨日', value: 'yesterday' },
+    { label: '7日', value: '7days' },
+    { label: '15日', value: '15days' },
+    { label: '30日', value: '30days' }
+];
+
+const handleSearch = () => {
+    console.log('Search clicked', activeRange.value, dateRange.value);
+};
+
+// Mock Data
+const redPacketStats = [
+    { label: '红包拉动成交金额 (元)', value: '1999.9' },
+    { label: '参与人数 (人)', value: '200' },
+    { label: '客单价(元)', value: '200' },
+    { label: '订单覆盖率', value: '20%' },
+    { label: '红包过期率', value: '20%' },
+    { label: '红包使用数(张)', value: '200' },
+    { label: '红包领取金额(元)', value: '200' },
+    { label: '红包消耗金额 (元)', value: '200.00' },
+    { label: '红包使用率', value: '0.10%' },
+    { label: '红包待使用金额(元)', value: '200' },
+    { label: '红包待使用量(张)', value: '200' },
+    { label: '红包领取数(张)', value: '200' }
+];
+
+const shareStats = [
+    { label: '营销支付金额', value: '1999.99 (元)' },
+    { label: '营销优惠金额', value: '200.00 (元)' },
+    { label: '参与人数', value: '200.00 (人)' },
+    { label: '支付订单数', value: '200.00 (单)' },
+    { label: '客单价', value: '200.00 (元)' },
+    { label: '助力人数', value: '200.00 (人)' }
+];
+
+const fullReductionStats = [
+    { label: '营销支付金额', value: '1999.99 (元)' },
+    { label: '营销优惠金额', value: '200.00 (元)' },
+    { label: '参与人数', value: '200.00 (人)' },
+    { label: '支付订单数', value: '200.00 (单)' },
+    { label: '客单价', value: '200.00 (元)' }
+];
+
+const funnelData1 = {
+    visitorCount: 32,
+    paymentCount: 32,
+    unitPrice: 32,
+    conversionRate: '65%'
+};
+
+const funnelData2 = {
+    visitorCount: 32,
+    paymentCount: 32,
+    unitPrice: 32,
+    conversionRate: '65%'
+};
+
+const funnelData3 = {
+    visitorCount: 32,
+    paymentCount: 32,
+    unitPrice: 32,
+    conversionRate: '65%'
+};
+
+</script>
+
+<style scoped lang="scss">
+.filter-container {
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+
+    .date-ranges {
+        margin-right: 15px;
+
+        .el-button {
+            margin-right: 5px;
+            margin-left: 0;
+        }
+    }
+
+    .date-picker-wrapper {
+        margin-right: 15px;
+    }
+}
+</style>

+ 140 - 0
src/views/statsAnalysis/search/components/SearchEffectModal.vue

@@ -0,0 +1,140 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="800px"
+    @close="handleClose"
+  >
+    <div class="modal-content">
+      <!-- Product Effect Section -->
+      <div class="section">
+        <div class="table-wrapper">
+           <el-table :data="productData" border size="small">
+             <el-table-column label="引导商品效果" min-width="200">
+                <template #default="{ row }">
+                  <div class="product-info">
+                    <img :src="row.image" class="product-img" />
+                    <div class="product-details">
+                      <div class="name">{{ row.name }}</div>
+                      <div class="meta">ISBN: {{ row.isbn }}</div>
+                      <div class="meta">价格: {{ row.price }} 库存: {{ row.stock }}</div>
+                    </div>
+                  </div>
+                </template>
+             </el-table-column>
+             <el-table-column prop="visitors" label="带来的访客数" sortable width="120" />
+             <el-table-column prop="views" label="带来的浏览量" sortable width="120" />
+             <el-table-column prop="buyers" label="引导下单买家数" sortable width="140" />
+             <el-table-column prop="conversion" label="引导下单转化率" sortable width="140" />
+           </el-table>
+           
+           <div class="pagination">
+             <el-pagination 
+               layout="total, prev, pager, next, jumper" 
+               :total="100" 
+               small
+             />
+           </div>
+        </div>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue';
+import { Goods } from '@element-plus/icons-vue';
+
+const props = defineProps({
+  modelValue: Boolean,
+  rowData: Object
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+const visible = ref(false);
+
+watch(() => props.modelValue, (val) => {
+  visible.value = val;
+});
+
+const handleClose = () => {
+  emit('update:modelValue', false);
+};
+
+const title = computed(() => {
+  return props.rowData ? `搜索词详情: ${props.rowData.term} - 商品效果` : '商品效果详情';
+});
+
+// Mock Product Data
+const productData = ref([
+  { 
+    image: 'https://via.placeholder.com/60', 
+    name: '马克思主义基本原理', 
+    isbn: '9787123654587', 
+    price: '22.12', 
+    stock: 5,
+    visitors: 23,
+    views: 33,
+    buyers: 2,
+    conversion: '8.7%'
+  },
+  { 
+    image: 'https://via.placeholder.com/60', 
+    name: '马克思主义基本原理', 
+    isbn: '9787123654587', 
+    price: '22.12', 
+    stock: 5,
+    visitors: 23,
+    views: 33,
+    buyers: 2,
+    conversion: '8.7%'
+  },
+  { 
+    image: 'https://via.placeholder.com/60', 
+    name: '马克思主义基本原理', 
+    isbn: '9787123654587', 
+    price: '22.12', 
+    stock: 5,
+    visitors: 23,
+    views: 33,
+    buyers: 2,
+    conversion: '8.7%'
+  }
+]);
+</script>
+
+<style scoped lang="scss">
+.product-info {
+  display: flex;
+  align-items: center;
+  
+  .product-img {
+    width: 60px;
+    height: 60px;
+    object-fit: cover;
+    margin-right: 10px;
+    border-radius: 4px;
+  }
+  
+  .product-details {
+    .name {
+      font-weight: bold;
+      color: #333; // Usually green in example but let's stick to standard
+      &.green { color: #4caf50; } // If needed
+      font-size: 14px;
+      margin-bottom: 5px;
+    }
+    .meta {
+      font-size: 12px;
+      color: #999;
+    }
+  }
+}
+
+.pagination {
+  margin-top: 15px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 142 - 0
src/views/statsAnalysis/search/components/SearchOverview.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="search-overview">
+    <div class="header">
+      <div class="title">搜索的使用次数</div>
+      <div class="subtitle">2024-06-01至2024-06-25</div>
+    </div>
+    
+    <div class="content">
+      <!-- Stats -->
+      <div class="stats-section">
+        <div class="stat-item">
+          <div class="label">今日</div>
+          <div class="value">369次</div>
+          <div class="compare down">对比昨日 59% ↓</div>
+        </div>
+        <div class="stat-item">
+          <div class="label">合计</div>
+          <div class="value">36009次</div>
+        </div>
+        <div class="stat-item">
+          <div class="label">人均搜索次数</div>
+          <div class="value">3600次</div>
+        </div>
+      </div>
+
+      <!-- Chart -->
+      <div class="chart-section">
+        <base-chart :option="chartOption" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import BaseChart from '@/components/Chart/BaseChart.vue';
+
+const chartOption = computed(() => ({
+  tooltip: {
+    trigger: 'axis'
+  },
+  grid: {
+    top: '10%',
+    bottom: '10%',
+    left: '3%',
+    right: '4%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: ['1日', '2日', '3日', '4日', '5日'],
+    axisTick: { alignWithLabel: true }
+  },
+  yAxis: {
+    type: 'value',
+    splitLine: { lineStyle: { type: 'dashed' } }
+  },
+  series: [
+    {
+      name: '搜索总次数',
+      type: 'line',
+      smooth: true,
+      data: [100, 140, 230, 100, 130],
+      itemStyle: { color: '#5b8ff9' },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0, y: 0, x2: 0, y2: 1,
+          colorStops: [
+            { offset: 0, color: 'rgba(91, 143, 249, 0.2)' },
+            { offset: 1, color: 'rgba(91, 143, 249, 0)' }
+          ]
+        }
+      }
+    }
+  ]
+}));
+</script>
+
+<style scoped lang="scss">
+.search-overview {
+  background-color: #f5f5f5;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+}
+
+.header {
+  margin-bottom: 20px;
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #333;
+  }
+  .subtitle {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+  }
+}
+
+.content {
+  display: flex;
+  align-items: center;
+
+  .stats-section {
+    flex: 0 0 40%;
+    display: flex;
+    justify-content: space-around;
+
+    .stat-item {
+      text-align: center;
+      
+      .label {
+        font-size: 14px;
+        color: #666;
+        margin-bottom: 10px;
+      }
+      
+      .value {
+        font-size: 24px;
+        font-weight: bold;
+        color: #333;
+        margin-bottom: 5px;
+      }
+      
+      .compare {
+        font-size: 12px;
+        &.down {
+          color: #52c41a; // Green for down usually means good? Or red? In finance Green is down (CN) or Up (US). 
+                          // Design shows green arrow down. Let's use green.
+        }
+      }
+    }
+  }
+
+  .chart-section {
+    flex: 1;
+    height: 200px;
+  }
+}
+</style>

+ 35 - 0
src/views/statsAnalysis/search/components/SearchTable.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="search-table">
+    <el-table :data="tableData" style="width: 100%" border>
+      <el-table-column prop="term" label="搜索词" />
+      <el-table-column prop="searchCount" label="搜索次数" sortable />
+      <el-table-column prop="cartCount" label="加购人数" sortable />
+      <el-table-column prop="favCount" label="商品收藏人数" sortable />
+      <el-table-column prop="payCount" label="支付买家数" sortable />
+      <el-table-column prop="conversionRate" label="支付转化率" sortable />
+      <el-table-column label="操作" width="200">
+        <template #default="scope">
+          <el-button link type="primary" @click="$emit('view-trend', scope.row)">[趋势]</el-button>
+          <el-button link type="warning" @click="$emit('view-effect', scope.row)">[商品效果]</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  tableData: {
+    type: Array,
+    default: () => []
+  }
+});
+
+defineEmits(['view-trend', 'view-effect']);
+</script>
+
+<style scoped>
+.search-table {
+  margin-top: 20px;
+}
+</style>

+ 70 - 0
src/views/statsAnalysis/search/components/SearchTrendModal.vue

@@ -0,0 +1,70 @@
+<template>
+    <el-dialog v-model="visible" :title="title" width="800px" @close="handleClose">
+        <div class="modal-content">
+            <!-- Trend Section -->
+            <div class="section">
+                <div class="chart-wrapper">
+                    <base-chart :option="trendOption" />
+                </div>
+            </div>
+        </div>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue';
+import { DataLine } from '@element-plus/icons-vue';
+import BaseChart from '@/components/Chart/BaseChart.vue';
+
+const props = defineProps({
+    modelValue: Boolean,
+    rowData: Object
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+const visible = ref(false);
+
+watch(() => props.modelValue, (val) => {
+    visible.value = val;
+});
+
+const handleClose = () => {
+    emit('update:modelValue', false);
+};
+
+const title = computed(() => {
+    return props.rowData ? `搜索词详情: ${props.rowData.term} - 趋势` : '趋势详情';
+});
+
+// Mock Trend Data
+const trendOption = computed(() => ({
+    tooltip: { trigger: 'axis' },
+    legend: { top: 0 },
+    grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+    xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: ['09-07', '09-08', '09-10', '09-11', '09-12', '09-13', '09-14']
+    },
+    yAxis: { type: 'value' },
+    series: [
+        { name: '搜索次数', type: 'line', smooth: true, data: [100, 120, 300, 100, 130, 210, 150], itemStyle: { color: '#5b8ff9' } },
+        { name: '加购人数', type: 'line', smooth: true, data: [150, 100, 200, 140, 100, 160, 110], itemStyle: { color: '#5ebcb9' } },
+        { name: '商品收藏人数', type: 'line', smooth: true, data: [150, 180, 110, 150, 200, 160, 220], itemStyle: { color: '#4caf50' } },
+        { name: '支付买家数', type: 'line', smooth: true, data: [100, 110, 220, 140, 150, 160, 130], itemStyle: { color: '#ffc107' } },
+        { name: '支付转化率', type: 'line', smooth: true, data: [100, 100, 120, 140, 140, 190, 180], itemStyle: { color: '#ff5722' } }
+    ]
+}));
+</script>
+
+<style scoped lang="scss">
+.section {
+    .chart-wrapper {
+        height: 300px;
+        background: #fafafa;
+        padding: 10px;
+        border-radius: 4px;
+    }
+}
+</style>

+ 92 - 0
src/views/statsAnalysis/search/index.vue

@@ -0,0 +1,92 @@
+<template>
+    <ele-page>
+        <ele-card :body-style="{ padding: '20px' }">
+            <!-- Search Filter Bar -->
+            <div class="filter-container">
+                <el-select v-model="searchType" placeholder="搜索词分类" style="width: 120px; margin-right: 15px;">
+                    <el-option label="搜索词分类" value="category" />
+                </el-select>
+
+                <div class="date-ranges">
+                    <el-button-group>
+                        <el-button type="primary">实时</el-button>
+                        <el-button>昨日</el-button>
+                        <el-button>7日</el-button>
+                        <el-button>15日</el-button>
+                        <el-button>30日</el-button>
+                    </el-button-group>
+                </div>
+
+                <div class="date-picker-wrapper">
+                    <el-date-picker v-model="dateRange" type="daterange" range-separator="-" start-placeholder="选择时间"
+                        end-placeholder="选择时间" size="default" />
+                </div>
+
+                <el-button type="primary">搜索</el-button>
+                <el-button>重置</el-button>
+            </div>
+
+            <!-- Overview Stats -->
+            <search-overview />
+
+            <!-- Data Table -->
+            <search-table :table-data="tableData" @view-trend="handleViewTrend" @view-effect="handleViewEffect" />
+
+            <!-- Trend Modal -->
+            <search-trend-modal v-model="trendModalVisible" :row-data="currentRow" />
+            
+            <!-- Effect Modal -->
+            <search-effect-modal v-model="effectModalVisible" :row-data="currentRow" />
+
+        </ele-card>
+    </ele-page>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import SearchOverview from './components/SearchOverview.vue';
+import SearchTable from './components/SearchTable.vue';
+import SearchTrendModal from './components/SearchTrendModal.vue';
+import SearchEffectModal from './components/SearchEffectModal.vue';
+
+const searchType = ref('category');
+const dateRange = ref([]);
+
+const trendModalVisible = ref(false);
+const effectModalVisible = ref(false);
+const currentRow = ref(null);
+
+const tableData = ref([
+    { term: '中医基础理论', searchCount: 23, cartCount: 5, favCount: 0, payCount: 2, conversionRate: '8.70%' },
+    { term: '大学语文', searchCount: 6, cartCount: 3, favCount: 1, payCount: 0, conversionRate: '0.00%' },
+    { term: '马克思主义发展史', searchCount: 6, cartCount: 3, favCount: 0, payCount: 3, conversionRate: '50.00%' },
+    { term: '高等数学', searchCount: 15, cartCount: 8, favCount: 2, payCount: 5, conversionRate: '33.33%' },
+    { term: 'Python编程', searchCount: 12, cartCount: 4, favCount: 3, payCount: 1, conversionRate: '8.33%' }
+]);
+
+const handleViewTrend = (row) => {
+    currentRow.value = row;
+    trendModalVisible.value = true;
+};
+
+const handleViewEffect = (row) => {
+    currentRow.value = row;
+    effectModalVisible.value = true;
+};
+</script>
+
+<style scoped lang="scss">
+.filter-container {
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+
+    .date-ranges {
+        margin-right: 15px;
+    }
+
+    .date-picker-wrapper {
+        margin-right: 15px;
+    }
+}
+</style>

+ 106 - 0
src/views/statsAnalysis/sellData/components/DistributionCharts.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="distribution-charts">
+    <div class="chart-box">
+      <div class="chart-title">支付方式占比</div>
+      <div class="chart-content">
+        <base-chart :option="paymentOption" />
+      </div>
+    </div>
+    <div class="chart-box">
+      <div class="chart-title">订单状态占比</div>
+      <div class="chart-content">
+        <base-chart :option="statusOption" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import BaseChart from '@/components/Chart/BaseChart.vue';
+
+const props = defineProps({
+  paymentData: {
+    type: Array,
+    default: () => []
+  },
+  statusData: {
+    type: Array,
+    default: () => []
+  }
+});
+
+const paymentOption = computed(() => ({
+  tooltip: {
+    trigger: 'item'
+  },
+  legend: {
+    top: '5%',
+    left: 'center'
+  },
+  series: [
+    {
+      name: '支付方式',
+      type: 'pie',
+      radius: '50%',
+      data: props.paymentData,
+      emphasis: {
+        itemStyle: {
+          shadowBlur: 10,
+          shadowOffsetX: 0,
+          shadowColor: 'rgba(0, 0, 0, 0.5)'
+        }
+      }
+    }
+  ]
+}));
+
+const statusOption = computed(() => ({
+  tooltip: {
+    trigger: 'item'
+  },
+  legend: {
+    top: '5%',
+    left: 'center'
+  },
+  series: [
+    {
+      name: '订单状态',
+      type: 'pie',
+      radius: '50%',
+      data: props.statusData,
+      emphasis: {
+        itemStyle: {
+          shadowBlur: 10,
+          shadowOffsetX: 0,
+          shadowColor: 'rgba(0, 0, 0, 0.5)'
+        }
+      }
+    }
+  ]
+}));
+</script>
+
+<style scoped lang="scss">
+.distribution-charts {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 20px;
+
+  .chart-box {
+    flex: 1;
+    background: #fff; // Or transparent if card handles bg
+    
+    .chart-title {
+      font-size: 16px;
+      font-weight: bold;
+      margin-bottom: 15px;
+      color: #333;
+    }
+
+    .chart-content {
+      height: 300px;
+    }
+  }
+}
+</style>

+ 110 - 0
src/views/statsAnalysis/sellData/components/OrderTrendChart.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="order-trend-chart">
+    <div class="chart-header">
+      <div class="title">订单数量</div>
+      <div class="legend">
+        <!-- Custom Legend if needed, or rely on ECharts legend -->
+      </div>
+    </div>
+    <div class="chart-container">
+      <base-chart :option="chartOption" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import BaseChart from '@/components/Chart/BaseChart.vue';
+
+const props = defineProps({
+  chartData: {
+    type: Object,
+    default: () => ({
+      dates: [],
+      orderCount: [],
+      paymentAmount: [],
+      conversionRate: [],
+      refundAmount: []
+    })
+  }
+});
+
+const chartOption = computed(() => ({
+  tooltip: {
+    trigger: 'axis'
+  },
+  legend: {
+    data: ['订单数', '支付金额', '转化率', '退款金额'],
+    top: 0
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: false,
+    data: props.chartData.dates
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      name: '订单数',
+      type: 'line',
+      smooth: true,
+      data: props.chartData.orderCount,
+      itemStyle: { color: '#5b8ff9' }
+    },
+    {
+      name: '支付金额',
+      type: 'line',
+      smooth: true,
+      data: props.chartData.paymentAmount,
+      itemStyle: { color: '#5ebcb9' }
+    },
+    {
+      name: '转化率',
+      type: 'line',
+      smooth: true,
+      data: props.chartData.conversionRate,
+      itemStyle: { color: '#4caf50' }
+    },
+    {
+      name: '退款金额',
+      type: 'line',
+      smooth: true,
+      data: props.chartData.refundAmount,
+      itemStyle: { color: '#ff9800' }
+    }
+  ]
+}));
+</script>
+
+<style scoped lang="scss">
+.order-trend-chart {
+  background: #fff;
+  padding: 20px 0;
+  margin-bottom: 20px;
+
+  .chart-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    .title {
+      font-size: 16px;
+      font-weight: bold;
+      color: #333;
+    }
+  }
+
+  .chart-container {
+    height: 350px;
+  }
+}
+</style>

+ 157 - 0
src/views/statsAnalysis/sellData/components/RefundStats.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="refund-stats">
+    <div class="header">
+      <div class="title">已完结退款</div>
+      <div class="filters">
+        <span class="range-text">统计时间: {{ dateRangeText }}</span>
+        <div class="actions">
+           <!-- Filters can be passed via slots or handled in parent, 
+                but UI shows them here. Let's make them visual only for now or simple props -->
+           <el-button-group size="small">
+             <el-button>昨日</el-button>
+             <el-button type="primary">7日</el-button>
+             <el-button>30日</el-button>
+           </el-button-group>
+           <el-date-picker size="small" type="daterange" style="width: 200px; margin: 0 10px;" />
+           <el-button type="primary" size="small">搜索</el-button>
+           <el-button size="small">重置</el-button>
+        </div>
+      </div>
+    </div>
+
+    <div class="cards-container">
+      <div v-for="(card, index) in refundData" :key="index" class="refund-card">
+        <div class="card-header">
+          <div class="indicator"></div>
+          <span class="card-title">{{ card.title }}</span>
+        </div>
+        
+        <div class="card-content">
+          <div class="row">
+            <span class="label">退款人数</span>
+            <span class="value">{{ card.refundUsers }}</span>
+          </div>
+          <div class="row">
+            <span class="label">退款子订单数</span>
+            <span class="value">{{ card.subOrders }}</span>
+          </div>
+          <div class="row">
+            <span class="label">订单退款率</span>
+            <span class="value">{{ card.refundRate }}</span>
+          </div>
+          
+          <div class="divider"></div>
+
+          <div class="row">
+            <span class="label">退款金额</span>
+            <span class="value">{{ card.refundAmount }}</span>
+          </div>
+          <div class="row">
+            <span class="label">金额退款率</span>
+            <span class="value">{{ card.amountRate }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  dateRangeText: {
+    type: String,
+    default: '2025-09-15 ~ 2025-10-14'
+  },
+  refundData: {
+    type: Array,
+    default: () => []
+  }
+});
+</script>
+
+<style scoped lang="scss">
+.refund-stats {
+  margin-top: 30px;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #333;
+  }
+
+  .filters {
+    display: flex;
+    align-items: center;
+
+    .range-text {
+      font-size: 12px;
+      color: #999;
+      margin-right: 20px;
+    }
+  }
+}
+
+.cards-container {
+  display: flex;
+  gap: 15px;
+  overflow-x: auto;
+}
+
+.refund-card {
+  flex: 0 0 240px;
+  background: #fff;
+  border: 1px solid #e6ebf5;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+
+  .card-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 15px;
+    
+    .indicator {
+      width: 4px;
+      height: 14px;
+      background-color: #409eff;
+      margin-right: 8px;
+      border-radius: 2px;
+    }
+    
+    .card-title {
+      font-weight: bold;
+      color: #333;
+    }
+  }
+
+  .card-content {
+    .row {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 10px;
+      font-size: 13px;
+      
+      .label {
+        color: #666;
+      }
+      .value {
+        color: #333;
+        font-weight: 500;
+      }
+    }
+
+    .divider {
+      height: 1px;
+      background-color: #eee;
+      margin: 15px 0;
+    }
+  }
+}
+</style>

+ 111 - 0
src/views/statsAnalysis/sellData/components/SalesStatCards.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="sales-stat-cards">
+    <!-- Top Status Cards -->
+    <div class="status-cards">
+      <div v-for="(item, index) in statusData" :key="index" class="status-card">
+        <div class="status-value">{{ item.value }}</div>
+        <div class="status-label">{{ item.label }}</div>
+      </div>
+    </div>
+
+    <!-- Main Sales Stats Grid -->
+    <div class="stats-grid">
+      <div v-for="(item, index) in salesData" :key="index" class="stat-card">
+        <div class="stat-header">{{ item.label }}</div>
+        <div class="stat-value">{{ item.value }}</div>
+        <div class="stat-compare">
+          昨日 {{ item.yesterdayValue }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  statusData: {
+    type: Array,
+    default: () => []
+  },
+  salesData: {
+    type: Array,
+    default: () => []
+  }
+});
+</script>
+
+<style scoped lang="scss">
+.sales-stat-cards {
+  margin-bottom: 20px;
+}
+
+.status-cards {
+  display: flex;
+  justify-content: space-between;
+  background-color: #f5f5f5;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+
+  .status-card {
+    text-align: center;
+    flex: 1;
+    position: relative;
+
+    &:not(:last-child)::after {
+      content: '';
+      position: absolute;
+      right: 0;
+      top: 10%;
+      height: 80%;
+      width: 1px;
+      background-color: #e0e0e0;
+    }
+
+    .status-value {
+      font-size: 24px;
+      font-weight: bold;
+      color: #333;
+      margin-bottom: 5px;
+    }
+
+    .status-label {
+      font-size: 14px;
+      color: #666;
+    }
+  }
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 15px;
+
+  .stat-card {
+    background-color: #f5f5f5;
+    padding: 20px;
+    border-radius: 8px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+
+    .stat-header {
+      font-size: 14px;
+      color: #666;
+      margin-bottom: 10px;
+    }
+
+    .stat-value {
+      font-size: 24px;
+      font-weight: bold;
+      color: #333;
+      margin-bottom: 5px;
+    }
+
+    .stat-compare {
+      font-size: 12px;
+      color: #999;
+    }
+  }
+}
+</style>

+ 120 - 0
src/views/statsAnalysis/sellData/index.vue

@@ -0,0 +1,120 @@
+<template>
+  <ele-page>
+    <ele-card :body-style="{ padding: '20px' }">
+      <!-- Filters -->
+      <div class="filter-container">
+        <div class="date-ranges">
+          <el-button-group>
+            <el-button type="primary">实时</el-button>
+            <el-button>昨日</el-button>
+            <el-button>7日</el-button>
+            <el-button>15日</el-button>
+            <el-button>30日</el-button>
+          </el-button-group>
+        </div>
+        <div class="date-picker-wrapper">
+          <el-date-picker v-model="dateRange" type="daterange" range-separator="到" start-placeholder="开始时间"
+            end-placeholder="结束时间" size="default" />
+        </div>
+        <el-button type="primary" class="search-btn">搜索</el-button>
+        <el-button>重置</el-button>
+      </div>
+
+      <!-- Stats Cards -->
+      <sales-stat-cards :status-data="statusData" :sales-data="salesData" />
+
+      <!-- Order Trend Chart -->
+      <order-trend-chart :chart-data="trendChartData" />
+
+      <!-- Distribution Charts -->
+      <distribution-charts :payment-data="paymentMethodData" :status-data="orderStatusData" />
+
+      <!-- Refund Stats -->
+      <refund-stats :refund-data="refundData" />
+
+    </ele-card>
+  </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import SalesStatCards from './components/SalesStatCards.vue';
+import OrderTrendChart from './components/OrderTrendChart.vue';
+import DistributionCharts from './components/DistributionCharts.vue';
+import RefundStats from './components/RefundStats.vue';
+
+const dateRange = ref([]);
+
+// Mock Data
+const statusData = [
+  { label: '待付款', value: 11 },
+  { label: '待发货', value: 11 },
+  { label: '待售后', value: 11 },
+  { label: '待处理投诉', value: 11 }
+];
+
+const salesData = [
+  { label: '支付金额', value: '55366', yesterdayValue: '53669' },
+  { label: '访客数', value: '55366', yesterdayValue: '53669' },
+  { label: '支付订单数', value: '55366', yesterdayValue: '53669' },
+  { label: '支付转化率', value: '55366', yesterdayValue: '53669' },
+  { label: '商品浏览量', value: '55366', yesterdayValue: '53669' },
+  { label: '客单价', value: '55366', yesterdayValue: '53669' },
+  { label: '支付买家数', value: '55366', yesterdayValue: '53669' },
+  { label: '加购商品数', value: '55366', yesterdayValue: '53669' },
+  { label: '收藏商品数', value: '55366', yesterdayValue: '53669' },
+  { label: '在途订单数', value: '55366', yesterdayValue: '53669' },
+  { label: '在途金额', value: '55366', yesterdayValue: '53669' },
+  { label: '老客复购率', value: '55366', yesterdayValue: '53669' },
+  { label: '老客复购金额', value: '55366', yesterdayValue: '53669' },
+  { label: '老客复购人数', value: '55366', yesterdayValue: '53669' },
+  { label: '净支付金额', value: '55366', yesterdayValue: '53669' }
+];
+
+const trendChartData = reactive({
+  dates: ['15日', '16日', '17日', '18日', '19日', '20日', '21日', '22日', '23日', '24日', '25日', '26日', '27日'],
+  orderCount: [100, 120, 200, 100, 130, 150, 180, 220, 150, 170, 110, 160, 210],
+  paymentAmount: [150, 100, 230, 110, 100, 170, 210, 110, 108, 150, 190, 180, 170],
+  conversionRate: [150, 130, 180, 150, 210, 210, 210, 220, 180, 130, 160, 180, 170],
+  refundAmount: [160, 220, 230, 160, 210, 150, 160, 150, 170, 200, 170, 220, 100]
+});
+
+const paymentMethodData = [
+  { value: 100, name: '微信支付', itemStyle: { color: '#5b8ff9' } },
+  { value: 150, name: '支付宝支付', itemStyle: { color: '#5ebcb9' } },
+  { value: 224, name: '余额支付', itemStyle: { color: '#4caf50' } }
+];
+
+const orderStatusData = [
+  { value: 122, name: '待发货', itemStyle: { color: '#ff9800' } },
+  { value: 150, name: '已退款', itemStyle: { color: '#5ebcb9' } },
+  { value: 170, name: '待收货', itemStyle: { color: '#4caf50' } },
+  { value: 174, name: '已取消', itemStyle: { color: '#f44336' } },
+  { value: 175, name: '已完成', itemStyle: { color: '#e91e63' } },
+  { value: 100, name: '已发货', itemStyle: { color: '#5b8ff9' } }
+];
+
+const refundData = [
+  { title: '全部', refundUsers: 2720, subOrders: 3089, refundRate: '10.63%', refundAmount: 36738.53, amountRate: '11.03%' },
+  { title: '退货退款', refundUsers: 339, subOrders: 354, refundRate: '1.22%', refundAmount: 36738.53, amountRate: '11.03%' },
+  { title: '已收货退款', refundUsers: 28, subOrders: 3089, refundRate: '10.63%', refundAmount: 36738.53, amountRate: '11.03%' },
+  { title: '未收货退款', refundUsers: 2720, subOrders: 3089, refundRate: '10.63%', refundAmount: 36738.53, amountRate: '11.03%' },
+  { title: '未发货退款', refundUsers: 2720, subOrders: 3089, refundRate: '10.63%', refundAmount: 36738.53, amountRate: '11.03%' }
+];
+</script>
+
+<style scoped lang="scss">
+.filter-container {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+
+  .date-ranges {
+    margin-right: 15px;
+  }
+
+  .date-picker-wrapper {
+    margin-right: 15px;
+  }
+}
+</style>