1
0

2 コミット a60e9ffe28 ... 7636d17f87

作者 SHA1 メッセージ 日付
  ylong 7636d17f87 feat(priceWorkbench): implement task management API and enhance UI components 1 週間 前
  ylong d8f8f3d901 refactor(shareDiscount): improve metrics display and update data handling 1 週間 前

+ 140 - 0
src/api/priceWorkbench/index.js

@@ -0,0 +1,140 @@
+import request from '@/utils/request';
+import { download, checkDownloadRes } from '@/utils/common';
+
+function unwrap(res) {
+    const body = res.data || {};
+    if (body.code === 200) {
+        return body;
+    }
+    return Promise.reject(new Error(body.msg || '请求失败'));
+}
+
+/** 去掉空查询条件,避免整型参数收到空字符串 */
+export function cleanParams(params = {}) {
+    const out = {};
+    Object.entries(params).forEach(([key, value]) => {
+        if (value === '' || value === undefined || value === null) {
+            return;
+        }
+        out[key] = value;
+    });
+    delete out.search;
+    delete out.isReset;
+    return out;
+}
+
+/** CommonTable pages → offset/limitSize(周转率、审核、待处理明细) */
+export function withOffsetLimit(pages = {}, extra = {}) {
+    const pageNum = Number(pages.pageNum || pages.page || 1);
+    const pageSize = Number(pages.pageSize || pages.limit || 10);
+    return cleanParams({
+        ...extra,
+        offset: (pageNum - 1) * pageSize,
+        limitSize: pageSize
+    });
+}
+
+export function actionMessage(res, fallback = '操作成功') {
+    if (typeof res?.data === 'string' && res.data) {
+        return res.data;
+    }
+    return res?.msg || fallback;
+}
+
+export function listTaskRules() {
+    return request.get('/price/taskRule/list').then(unwrap);
+}
+
+export function addTaskRule(data) {
+    return request.post('/price/taskRule/add', data).then(unwrap);
+}
+
+export function deleteTaskRule(ruleId) {
+    return request.post(`/price/taskRule/delete/${ruleId}`).then(unwrap);
+}
+
+export function generateTask(ruleId) {
+    return request.post(`/price/taskRule/generateTask/${ruleId}`).then(unwrap);
+}
+
+export function getTaskStat() {
+    return request.get('/price/task/stat').then(unwrap);
+}
+
+export function getTaskSubList(taskId) {
+    return request.get(`/price/task/subList/${taskId}`).then(unwrap);
+}
+
+export function getTaskLogs(taskId, params) {
+    return request
+        .get(`/price/task/logList/${taskId}`, { params: cleanParams(params) })
+        .then(unwrap);
+}
+
+export function getHandlerOptions() {
+    return request.get('/price/task/handlerOptions').then(unwrap);
+}
+
+export function assignTask(data) {
+    return request.post('/price/task/assign', data).then(unwrap);
+}
+
+export function revokeTask(taskId) {
+    return request.post(`/price/task/revoke/${taskId}`).then(unwrap);
+}
+
+export function revokeSubTask(subId) {
+    return request.post(`/price/task/revokeSub/${subId}`).then(unwrap);
+}
+
+export function submitTaskPrice(data) {
+    return request.post('/price/task/submit', data).then(unwrap);
+}
+
+export function listMyTasks(params) {
+    return request
+        .get('/price/task/myList', { params: cleanParams(params) })
+        .then(unwrap);
+}
+
+export function listMyTaskItems(params) {
+    return request
+        .get('/price/task/myItemList', { params: cleanParams(params) })
+        .then(unwrap);
+}
+
+export function listAudit(params) {
+    return request
+        .get('/price/audit/list', { params: cleanParams(params) })
+        .then(unwrap);
+}
+
+export function approveAudit(data) {
+    return request.post('/price/audit/approve', data).then(unwrap);
+}
+
+export function listTurnover(params) {
+    return request
+        .get('/book/bookTurnover/list', { params: cleanParams(params) })
+        .then(unwrap);
+}
+
+export function getTurnoverHistory(params) {
+    return request
+        .get('/book/bookTurnover/history', { params: cleanParams(params) })
+        .then(unwrap);
+}
+
+export function importTurnover(file) {
+    const formData = new FormData();
+    formData.append('file', file);
+    return request.post('/book/bookTurnover/import', formData).then(unwrap);
+}
+
+export async function downloadTurnoverTemplate() {
+    const res = await request.post('/book/bookTurnover/importTemplate', null, {
+        responseType: 'blob'
+    });
+    await checkDownloadRes(res);
+    download(res.data, `库存周转率导入模板_${Date.now()}.xlsx`);
+}

+ 7 - 1
src/components/CommonPage/ProSearch2.vue

@@ -26,6 +26,7 @@
                 <el-button style="width: 90px" type="info" @click="reset"
                     >重置</el-button
                 >
+                <slot name="after-reset" />
             </div>
         </template>
     </pro-form>
@@ -90,7 +91,12 @@
         search(true);
     };
 
+    function getData() {
+        return { ...form };
+    }
+
     defineExpose({
-        setData
+        setData,
+        getData
     });
 </script>

+ 69 - 54
src/views/marketing/shareDiscount/fission/index.vue

@@ -93,22 +93,14 @@
 
             <!-- Numbers Cards -->
             <div class="number-metrics mb-6">
-                <el-row :gutter="20">
-                    <el-col
-                        :span="4"
-                        v-for="(item, index) in numberMetricsData"
-                        :key="index"
-                    >
-                        <el-card shadow="hover" class="bg-gray-50">
-                            <div class="text-2xl font-bold mb-1 text-center">{{
-                                item.value
-                            }}</div>
-                            <div class="text-sm text-gray-500 text-center">{{
-                                item.label
-                            }}</div>
-                        </el-card>
-                    </el-col>
-                </el-row>
+                <div
+                    v-for="(item, index) in numberMetricsData"
+                    :key="index"
+                    class="number-card"
+                >
+                    <div class="number-value">{{ item.value }}</div>
+                    <div class="number-label">{{ item.label }}</div>
+                </div>
             </div>
 
             <!-- Chart Section -->
@@ -130,11 +122,11 @@
     defineOptions({ name: 'ShareDiscountFission' });
 
     // State
-    const activePeriod = ref('30d');
+    const activePeriod = ref('');
     const chartRef = ref(null);
     let chart = null;
     const dateRange = ref([
-        dayjs().subtract(29, 'day').format('YYYY-MM-DD'),
+        dayjs().format('YYYY-MM-DD'),
         dayjs().format('YYYY-MM-DD')
     ]);
     const loading = ref(false);
@@ -151,18 +143,6 @@
         return str.includes('%') ? str : `${str}%`;
     };
 
-    const formatMoney = (value) => {
-        if (value === null || value === undefined || value === '') {
-            return '¥0';
-        }
-        const num = Number(value);
-        if (Number.isNaN(num)) return '¥0';
-        return `¥${num.toLocaleString('zh-CN', {
-            minimumFractionDigits: 0,
-            maximumFractionDigits: 2
-        })}`;
-    };
-
     const resultData = computed(() => statisticData.value?.data || {});
 
     const displayDateRange = computed(() => {
@@ -177,34 +157,34 @@
         const data = resultData.value;
         return [
             {
-                title: '降价助力人数',
-                value: data.reduceHelpNum ?? 0,
+                title: '今日助力人数',
+                value: `${data.reduceHelpNum ?? 0}人`,
                 change: formatComparison(data.reduceHelpNumComparison)
             },
             {
-                title: '参与活动订单数',
-                value: data.reduceOrderNum ?? 0,
+                title: '今日参与活动订单数',
+                value: `${data.reduceOrderNum ?? 0}单`,
                 change: formatComparison(data.reduceOrderNumComparison)
             },
             {
-                title: '活动加购次数',
-                value: data.reduceCartNum ?? 0,
-                change: formatComparison(data.reduceCartNumComparison)
+                title: '今日活动订单浏览量',
+                value: `${data.bookViewTimes ?? 0}次`,
+                change: formatComparison(data.bookViewTimesComparison)
             },
             {
-                title: '活动加购参与占比',
-                value: formatPercent(data.cartJoinRate),
-                change: formatComparison(data.cartJoinRateComparison)
+                title: '参与占比/活动参与浏览量',
+                value: formatPercent(data.viewJoinRate),
+                change: formatComparison(data.viewJoinRateComparison)
             },
             {
-                title: '助力新用户占比',
-                value: formatPercent(data.newUserRate),
-                change: formatComparison(data.newUserRateComparison)
+                title: '今日助力使用产品占比',
+                value: formatPercent(data.helpNewUserRate),
+                change: formatComparison(data.helpNewUserRateComparison)
             },
             {
                 title: '裂变转化率',
-                value: formatPercent(data.conversionRate),
-                change: formatComparison(data.conversionRateComparison)
+                value: formatPercent(data.fissionConversionRate),
+                change: formatComparison(data.fissionConversionRateComparison)
             }
         ];
     });
@@ -212,12 +192,15 @@
     const numberMetricsData = computed(() => {
         const data = resultData.value;
         return [
-            { label: '降价活动分享人数', value: data.reduceShareNum ?? 0 },
+            { label: '参与用户数', value: data.reduceShareNum ?? 0 },
             { label: '助力次数', value: data.reduceHelpTimes ?? 0 },
-            { label: '订单总本书', value: data.orderBookNum ?? 0 },
-            { label: '订单总金额', value: formatMoney(data.orderTotalMoney) },
-            { label: '降价总金额', value: formatMoney(data.reduceTotalMoney) },
-            { label: '累计实付金额', value: formatMoney(data.orderPayMoney) }
+            { label: '订单总本数', value: data.orderBookNum ?? 0 },
+            { label: '参与活动本数', value: data.reduceBookNum ?? 0 },
+            { label: '参与活动总金额', value: data.orderTotalMoney ?? 0 },
+            { label: '订单带收益金额', value: data.orderPayMoney ?? 0 },
+            { label: '降价带收益金额', value: data.reduceTotalMoney ?? 0 },
+            { label: '实际降价金额', value: data.actualReduceMoney ?? 0 },
+            { label: '加购次数', value: data.reduceCartNum ?? 0 }
         ];
     });
 
@@ -288,7 +271,12 @@
     };
 
     const resetFilters = () => {
-        changePeriod('30d');
+        activePeriod.value = '';
+        dateRange.value = [
+            dayjs().format('YYYY-MM-DD'),
+            dayjs().format('YYYY-MM-DD')
+        ];
+        fetchData();
     };
 
     const fetchData = async () => {
@@ -329,10 +317,11 @@
 
         chart.setOption({
             tooltip: { trigger: 'axis' },
+            legend: { data: ['分享人数趋势'], bottom: 0 },
             grid: {
                 left: '3%',
                 right: '4%',
-                bottom: '3%',
+                bottom: '12%',
                 containLabel: true
             },
             xAxis: {
@@ -343,7 +332,7 @@
             yAxis: { type: 'value', min: 0 },
             series: [
                 {
-                    name: '分享人数',
+                    name: '分享人数趋势',
                     type: 'line',
                     smooth: true,
                     data: chartData.value.values,
@@ -372,9 +361,35 @@
 
 <style scoped>
     .metric-card {
-        height: 140px;
+        height: 150px;
         display: flex;
         flex-direction: column;
         justify-content: center;
     }
+
+    .number-metrics {
+        display: grid;
+        grid-template-columns: repeat(5, minmax(0, 1fr));
+        gap: 12px;
+    }
+
+    .number-card {
+        background: #f7f8fa;
+        border-radius: 8px;
+        padding: 16px 8px;
+        text-align: center;
+    }
+
+    .number-value {
+        font-size: 22px;
+        font-weight: 700;
+        color: #f6a623;
+        margin-bottom: 8px;
+        word-break: break-all;
+    }
+
+    .number-label {
+        font-size: 13px;
+        color: #606266;
+    }
 </style>

+ 4 - 4
src/views/priceWorkbench/priceAudit/components/page-search.vue

@@ -21,7 +21,7 @@
         { type: 'input', label: '书名', prop: 'bookName' },
         { type: 'input', label: 'ISBN', prop: 'isbn' },
         { type: 'input', label: '作者', prop: 'author' },
-        { type: 'input', label: '出版社', prop: 'publisher' },
+        { type: 'input', label: '出版社', prop: 'publish' },
         {
             type: 'daterange',
             label: '出版时间',
@@ -53,7 +53,7 @@
                 }
             }
         },
-        { type: 'input', label: '处理人', prop: 'handler' },
+        { type: 'input', label: '处理人ID', prop: 'handlerId' },
         {
             type: 'dictSelect',
             label: '图书类型',
@@ -85,12 +85,12 @@
         bookName: '',
         isbn: '',
         author: '',
-        publisher: '',
+        publish: '',
         pubDateStart: '',
         pubDateEnd: '',
         minPrice: void 0,
         maxPrice: void 0,
-        handler: '',
+        handlerId: '',
         bookTag: '',
         minDiscount: void 0,
         maxDiscount: void 0

+ 249 - 119
src/views/priceWorkbench/priceAudit/index.vue

@@ -15,9 +15,9 @@
                     v-model="searchType"
                     class="audit-tabs"
                 >
-                    <el-radio-button label="全部" value="0" />
-                    <el-radio-button label="待审核" value="1" />
-                    <el-radio-button label="已完成" value="2" />
+                    <el-radio-button label="全部" :value="0" />
+                    <el-radio-button label="待审核" :value="1" />
+                    <el-radio-button label="已完成" :value="2" />
                 </el-radio-group>
             </template>
 
@@ -34,109 +34,112 @@
 
             <template #info="{ row }">
                 <div class="flex flex-col text-sm">
-                    <div class="mb-1">
-                        <span class="font-bold">书名: </span>{{ row.bookName }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">作者: </span>{{ row.author }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">ISBN: </span>{{ row.isbn }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">出版社: </span
-                        >{{ row.publisher }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">出版时间: </span
-                        >{{ row.pubDate }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">所属类型: </span>{{ row.type }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">回收状态: </span
-                        >{{ row.recycleStatus }}
+                    <div class="info-basic-grid">
+                        <div><span class="font-bold">书名: </span>{{ row.bookName }}</div>
+                        <div><span class="font-bold">作者: </span>{{ row.author }}</div>
+                        <div><span class="font-bold">ISBN: </span>{{ row.isbn }}</div>
+                        <div><span class="font-bold">出版社: </span>{{ row.publish }}</div>
+                        <div><span class="font-bold">出版时间: </span>{{ row.pubDate }}</div>
+                        <div><span class="font-bold">所属类型: </span>{{ row.bookTagName }}</div>
+                        <div><span class="font-bold">回收状态: </span>{{ row.recycleStatusName }}</div>
                     </div>
                     <div class="mt-2 border-t pt-2">
                         <span class="mr-4">定价: {{ row.price }}</span>
                         <span class="mr-4"
                             >回收折扣: {{ row.recycleDiscount }}折
                             <span class="text-red-500 text-xs"
-                                >建议0.5-0.6折</span
+                                >{{ row.sugDiscountStr || '' }}</span
                             ></span
                         >
                         <span>回收价格: {{ row.recyclePrice }}</span>
                     </div>
                     <div class="mb-1">
                         <span class="text-green-500 text-xs"
-                            >谢程娟: 2025-06-15</span
+                            >{{ row.productPriceChangeLog?.operatorName || '' }} {{ row.productPriceChangeLog?.createTime || '' }}</span
                         >
                     </div>
                     <div class="mb-1">
-                        <span class="mr-4">销售价格: {{ row.salesPrice }}</span>
+                        <span class="mr-4">销售价格: {{ row.oldPrice }}</span>
                         <span class="text-green-500 text-xs"
-                            >谢程娟: 2025-06-15</span
+                            >{{ row.productPriceChangeLog?.operatorName || '' }} {{ row.productPriceChangeLog?.createTime || '' }}</span
                         >
                     </div>
                     <div class="mb-1">
                         <span
-                            >(已回收数量: {{ row.recycledCount }} 当前库存:
-                            {{ row.currentStock }})</span
+                            >(已回收数量: {{ row.recycleNum }} 当前库存:
+                            {{ row.stockNum }})</span
                         >
                     </div>
                 </div>
             </template>
 
-            <template #newPrice="{ row }">
-                <div class="flex flex-col items-center">
-                    <div class="flex items-center text-xl font-bold">
-                        {{ row.newPrice }}
-                        <el-icon class="ml-2 cursor-pointer text-red-500">
-                            <Edit />
-                        </el-icon>
-                    </div>
-                    <el-button size="small" type="info" plain class="mt-1"
-                        >填入原售价</el-button
-                    >
+            <template #priceHandler="{ row }">
+                <div class="price-handler-cell text-sm">
+                    <div><span class="label">当前售价:</span>{{ row.oldPrice ?? row.productPrice ?? '-' }}</div>
+                    <div><span class="label">处理人:</span>{{ row.handlerName || '-' }}</div>
+                    <div><span class="label">分配时间:</span>{{ row.assignTime || '-' }}</div>
                 </div>
             </template>
 
-            <template #action="{ row }">
-                <div class="flex flex-col items-center gap-2">
-                    <el-button
-                        color="#951d1d"
-                        size="small"
-                        @click="handleViewUrl(row, 'kw')"
-                        >查看孔网</el-button
-                    >
+            <template #newPrice="{ row }">
+                <div class="new-price-cell">
+                    <el-input-number
+                        v-model="row._finalPrice"
+                        :min="0"
+                        :precision="2"
+                        :step="0.01"
+                        :controls="false"
+                        class="new-price-input"
+                    />
                     <el-button
-                        color="#f37607"
+                        type="info"
+                        plain
                         size="small"
-                        @click="handleViewUrl(row, 'tb')"
-                        >查看淘宝</el-button
-                    >
-                    <el-button color="#4095e5" size="small"
-                        >设置独立参数</el-button
-                    >
-                    <el-button type="success" size="small">同意</el-button>
+                        class="fill-old-btn"
+                        @click="fillOldPrice(row)"
+                    >填入原售价</el-button>
+                </div>
+            </template>
+
+            <template #action="{ row }">
+                <div class="action-btns">
+                    <el-button color="#951d1d" class="action-btn" @click="handleViewUrl(row, 'kw')">查看孔网</el-button>
+                    <el-button color="#f37607" class="action-btn" @click="handleViewUrl(row, 'tb')">查看淘宝</el-button>
+                    <el-button color="#4095e5" class="action-btn" @click="handleSetParams(row)">设置独立参数</el-button>
+                    <template v-if="Number(searchType) !== 2">
+                        <el-button type="success" class="action-btn" :loading="approvingId === row.itemId && actionType === 'approve'" @click="confirmApprove(row)">同意</el-button>
+                        <el-button type="danger" class="action-btn" :loading="approvingId === row.itemId && actionType === 'reject'" @click="confirmReject(row)">拒绝</el-button>
+                    </template>
                 </div>
             </template>
         </common-table>
+
+        <set-params ref="setParamsRef" @refresh="reload" />
     </ele-page>
 </template>
 
 <script setup>
     import { ref, reactive } from 'vue';
-    import { Edit } from '@element-plus/icons-vue';
+    import { ElMessage, ElMessageBox } from 'element-plus';
     import CommonTable from '@/components/CommonPage/CommonTable.vue';
     import PageSearch from './components/page-search.vue';
+    import SetParams from '@/views/recycle/components/set-params.vue';
+    import request from '@/utils/request';
+    import {
+        actionMessage,
+        approveAudit,
+        cleanParams,
+        withOffsetLimit
+    } from '@/api/priceWorkbench';
 
     defineOptions({ name: 'PriceAudit' });
 
     // Component Refs
     const pageRef = ref(null);
-    const searchType = ref('0');
+    const setParamsRef = ref(null);
+    const searchType = ref(1);
+    const approvingId = ref(null);
+    const actionType = ref('');
 
     const columns = ref([
         { type: 'selection', width: 50, align: 'center', fixed: 'left' },
@@ -144,33 +147,37 @@
         { label: '信息', prop: 'info', minWidth: 400, slot: 'info' },
         {
             label: '总回收量',
-            prop: 'totalRecycled',
+            prop: 'recycleTotalNum',
             width: 110,
             sortable: true,
             align: 'center'
         },
-        { label: '状态', prop: 'status', width: 100, align: 'center' },
-        { label: '分配时间', prop: 'assignTime', width: 160, align: 'center' },
+        { label: '状态', prop: 'itemStatusName', width: 100, align: 'center' },
         {
             label: '当前库存',
-            prop: 'currentStock',
+            prop: 'stockNum',
             width: 110,
             sortable: true,
             align: 'center'
         },
-        { label: '当前售价', prop: 'salesPrice', width: 100, align: 'center' },
-        { label: '处理人', prop: 'handler', width: 100, align: 'center' },
+        {
+            label: '售价/处理人',
+            prop: 'priceHandler',
+            width: 180,
+            slot: 'priceHandler',
+            align: 'left'
+        },
         {
             label: '新售价',
             prop: 'newPrice',
-            width: 150,
+            width: 130,
             slot: 'newPrice',
             align: 'center'
         },
         {
             label: '操作',
             prop: 'action',
-            width: 120,
+            width: 150,
             slot: 'action',
             fixed: 'right',
             align: 'center'
@@ -181,39 +188,35 @@
         fileName: '价格审核',
         cacheKey: 'priceAuditTable',
         params: {
-            searchType: '0'
+            tab: 1
         },
-        rowKey: 'id'
+        rowKey: 'itemId'
     });
 
-    // Mock datasource
-    const datasource = ({ page, limit, where, orders }) => {
-        return Promise.resolve({
-            code: 0,
-            msg: 'success',
-            count: 10,
-            data: Array.from({ length: 10 }).map((_, i) => ({
-                id: i + 1,
-                cover: 'https://img1.baidu.com/it/u=2356551695,3062334057&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=707',
-                bookName: '马克思主义基本原理',
-                author: '编写组',
-                isbn: '9787056025415',
-                publisher: '高等教育出版社',
-                pubDate: '2018-06',
-                type: '教材库',
-                recycleStatus: '正在回收',
-                price: 25,
-                recycleDiscount: 0.2,
-                recyclePrice: 0.5,
-                salesPrice: 6.5,
-                recycledCount: 200,
-                currentStock: 75 + i,
-                totalRecycled: 200,
-                status: i % 2 === 0 ? '待审核' : '已审核',
-                assignTime: '2024-06-15 15:06:11',
-                handler: 'zzz',
-                newPrice: 10.55
-            }))
+    const datasource = ({ pages, where, orders, sorter }) => {
+        const params = withOffsetLimit(pages, {
+            ...(pageConfig.params || {}),
+            ...cleanParams(where || {})
+        });
+        if (orders && orders.orderByColumn) {
+            params.orderType = sorter?.column?.columnKey || orders.orderByColumn;
+            params.orderWay = orders.isAsc === 'ascending' ? 'asc' : 'desc';
+        }
+        return request.get('/price/audit/list', { params }).then((res) => {
+            if (res.data.code !== 200) {
+                return Promise.reject(new Error(res.data.msg));
+            }
+            const body = res.data;
+            body.rows = (body.rows || []).map((row) => {
+                const raw =
+                    row.finalPrice ?? row.newPrice ?? row.oldPrice ?? row.productPrice;
+                const num = Number(raw);
+                return {
+                    ...row,
+                    _finalPrice: Number.isFinite(num) ? num : undefined
+                };
+            });
+            return body;
         });
     };
 
@@ -222,7 +225,68 @@
     }
 
     function handleStatusChange(value) {
-        pageRef.value?.reload({ searchType: value });
+        pageConfig.params.tab = Number(value);
+        pageRef.value?.reload({ tab: Number(value), search: true });
+    }
+
+    function fillOldPrice(row) {
+        row._finalPrice = row.oldPrice ?? row.productPrice;
+    }
+
+    function handleSetParams(row) {
+        setParamsRef.value?.handleOpen(row);
+    }
+
+    function confirmApprove(row) {
+        const price = row._finalPrice;
+        ElMessageBox.confirm(
+            `确认同意该书新售价为 ${price ?? '-'} 元吗?`,
+            '同意确认',
+            {
+                confirmButtonText: '同意',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }
+        )
+            .then(() => submitAudit(row, price, 'approve', '审核通过'))
+            .catch(() => {});
+    }
+
+    function confirmReject(row) {
+        const oldPrice = row.oldPrice ?? row.productPrice;
+        ElMessageBox.confirm(
+            `确认拒绝改价吗?将按原售价 ${oldPrice ?? '-'} 元回退完成。`,
+            '拒绝确认',
+            {
+                confirmButtonText: '拒绝',
+                cancelButtonText: '取消',
+                type: 'warning',
+                confirmButtonClass: 'el-button--danger'
+            }
+        )
+            .then(() => {
+                row._finalPrice = oldPrice;
+                return submitAudit(row, oldPrice, 'reject', '已拒绝改价');
+            })
+            .catch(() => {});
+    }
+
+    async function submitAudit(row, finalPrice, type, successText) {
+        approvingId.value = row.itemId;
+        actionType.value = type;
+        try {
+            const res = await approveAudit({
+                itemId: row.itemId,
+                finalPrice
+            });
+            ElMessage.success(actionMessage(res, successText));
+            reload();
+        } catch (e) {
+            ElMessage.error(e.message);
+        } finally {
+            approvingId.value = null;
+            actionType.value = '';
+        }
     }
 
     //查看淘宝和孔网
@@ -238,35 +302,101 @@
 </script>
 
 <style scoped>
-    /* 仿照截图中的Tabs样式 */
+    .audit-tabs {
+        display: inline-flex;
+        background: #f5f7fa;
+        border-radius: 6px;
+        padding: 3px;
+        margin-bottom: 4px;
+    }
+
     :deep(.audit-tabs) .el-radio-button__inner {
-        width: 200px;
-        /* 增加宽度 */
-        border-radius: 0;
-        border: none;
-        border-bottom: 2px solid transparent;
+        min-width: 88px;
+        height: 30px;
+        padding: 0 14px;
+        line-height: 30px;
+        border: none !important;
+        border-radius: 4px !important;
         background: transparent;
-        box-shadow: none;
-        color: #666;
+        box-shadow: none !important;
+        color: var(--el-text-color-regular);
+        font-size: 13px;
     }
 
     :deep(.audit-tabs)
         .el-radio-button__original-radio:checked
         + .el-radio-button__inner {
-        background-color: #409eff;
-        /* 蓝色背景 */
-        color: white;
-        border-radius: 0;
-        /* 保持方形或自定义圆角 */
-        box-shadow: none;
+        background: var(--el-color-primary);
+        color: #fff;
+        box-shadow: none !important;
     }
 
-    /* 整体背景可能需要调整,这里仅做基本样式覆盖 */
-    .audit-tabs {
+    :deep(.audit-tabs) .el-radio-button:first-child .el-radio-button__inner,
+    :deep(.audit-tabs) .el-radio-button:last-child .el-radio-button__inner {
+        border-radius: 4px !important;
+    }
+
+    .new-price-cell {
         display: flex;
-        background-color: #fff;
-        border-bottom: 1px solid #eee;
-        margin-bottom: 10px;
+        flex-direction: column;
+        align-items: center;
+        gap: 6px;
+    }
+
+    .new-price-input {
+        width: 96px !important;
+    }
+
+    :deep(.new-price-input.el-input-number) {
+        width: 96px;
+    }
+
+    :deep(.new-price-input .el-input__wrapper) {
+        padding-left: 8px;
+        padding-right: 8px;
         width: 100%;
     }
+
+    :deep(.new-price-input .el-input__inner) {
+        text-align: center;
+        height: 32px;
+        line-height: 32px;
+    }
+
+    .fill-old-btn {
+        padding: 4px 8px !important;
+        min-height: 24px;
+        height: 24px;
+        font-size: 12px;
+    }
+
+    .action-btns {
+        display: flex;
+        flex-direction: column;
+        align-items: stretch;
+        gap: 8px;
+        width: 100%;
+    }
+
+    .action-btn {
+        margin: 0 !important;
+        width: 100%;
+        min-height: 34px;
+        font-size: 13px;
+    }
+
+    .info-basic-grid {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        column-gap: 16px;
+        row-gap: 4px;
+    }
+
+    .price-handler-cell {
+        line-height: 1.7;
+    }
+
+    .price-handler-cell .label {
+        color: var(--el-text-color-secondary);
+    }
 </style>

+ 36 - 49
src/views/priceWorkbench/stockTurnoverRate/components/history-dialog.vue

@@ -5,14 +5,15 @@
         v-model="visible"
         :body-style="{ padding: '20px' }"
     >
-        <div style="height: 400px">
+        <div v-loading="loading" style="height: 400px">
             <v-chart ref="chartRef" :option="option" autoresize />
         </div>
     </ele-modal>
 </template>
 
 <script setup>
-    import { ref, reactive, defineExpose } from 'vue';
+    import { ref, reactive } from 'vue';
+    import { ElMessage } from 'element-plus';
     import { useEcharts } from '@/utils/use-echarts';
     import VChart from 'vue-echarts';
     import { use } from 'echarts/core';
@@ -24,6 +25,7 @@
         LegendComponent,
         TitleComponent
     } from 'echarts/components';
+    import { getTurnoverHistory } from '@/api/priceWorkbench';
 
     use([
         CanvasRenderer,
@@ -36,13 +38,11 @@
 
     const visible = defineModel({ type: Boolean });
     const chartRef = ref(null);
-
+    const loading = ref(false);
     useEcharts([chartRef]);
 
     const option = reactive({
-        tooltip: {
-            trigger: 'axis'
-        },
+        tooltip: { trigger: 'axis' },
         legend: {
             data: ['历史周转率', '平均库存', '剩余可卖天数', '销量'],
             bottom: 0
@@ -53,53 +53,40 @@
             bottom: '10%',
             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'
-        },
+        xAxis: { type: 'category', boundaryGap: false, data: [] },
+        yAxis: { type: 'value' },
         series: [
-            {
-                name: '历史周转率',
-                type: 'line',
-                smooth: true,
-                data: [100, 132, 101, 134, 90, 230, 150]
-            },
-            {
-                name: '平均库存',
-                type: 'line',
-                smooth: true,
-                data: [150, 182, 191, 234, 100, 160, 110]
-            },
-            {
-                name: '剩余可卖天数',
-                type: 'line',
-                smooth: true,
-                data: [100, 100, 120, 140, 140, 190, 180]
-            },
-            {
-                name: '销量',
-                type: 'line',
-                smooth: true,
-                data: [200, 130, 145, 145, 230, 105, 180]
-            }
+            { name: '历史周转率', type: 'line', smooth: true, data: [] },
+            { name: '平均库存', type: 'line', smooth: true, data: [] },
+            { name: '剩余可卖天数', type: 'line', smooth: true, data: [] },
+            { name: '销量', type: 'line', smooth: true, data: [] }
         ]
     });
 
-    const setData = (row) => {
-        // Here you would fetch real data based on row.isbn or similar
-        console.log('Viewing history for:', row);
+    const setData = async (row) => {
+        if (!row?.isbn) {
+            ElMessage.warning('缺少 ISBN');
+            return;
+        }
+        loading.value = true;
+        try {
+            const res = await getTurnoverHistory({
+                isbn: row.isbn,
+                cycleType: row.cycleType
+            });
+            const rows = res.rows || res.data || [];
+            option.xAxis.data = rows.map((item) =>
+                (item.importTime || '').slice(5, 10)
+            );
+            option.series[0].data = rows.map((item) => item.turnoverRate);
+            option.series[1].data = rows.map((item) => item.avgStock);
+            option.series[2].data = rows.map((item) => item.remainSellDays);
+            option.series[3].data = rows.map((item) => item.salesNum);
+        } catch (e) {
+            ElMessage.error(e.message);
+        } finally {
+            loading.value = false;
+        }
     };
 
     defineExpose({ setData });

+ 23 - 19
src/views/priceWorkbench/stockTurnoverRate/components/import-dialog.vue

@@ -8,14 +8,10 @@
         <div class="flex mb-2">
             <div class="text-sm">请先下载'导入模板':</div>
             <el-button
-                @click="
-                    downloadOssLink(
-                        'https://shuhi.oss-cn-qingdao.aliyuncs.com/default/search_template.xlsx',
-                        '导入模板'
-                    )
-                "
                 link
                 type="primary"
+                :loading="tplLoading"
+                @click="handleDownloadTemplate"
                 >下载导入模板</el-button
             >
         </div>
@@ -54,8 +50,11 @@
 <script setup>
     import { ref } from 'vue';
     import { genFileId, ElMessage } from 'element-plus';
-    import { downloadOssLink } from '@/utils/common';
-    // import request from '@/utils/request'; // Mocking request
+    import {
+        actionMessage,
+        downloadTurnoverTemplate,
+        importTurnover
+    } from '@/api/priceWorkbench';
 
     const emit = defineEmits(['done']);
 
@@ -75,16 +74,21 @@
         uploadRef.value?.handleStart(file);
     }
 
-    //导入图书 (Mock)
+    const tplLoading = ref(false);
+
+    async function handleDownloadTemplate() {
+        tplLoading.value = true;
+        try {
+            await downloadTurnoverTemplate();
+        } catch (e) {
+            ElMessage.error(e.message || '模板下载失败');
+        } finally {
+            tplLoading.value = false;
+        }
+    }
+
     async function importBooks(file) {
-        // const formData = new FormData();
-        // formData.append('file', file);
-        // const res = await request.post('/book/bookInfo/exportByFile', formData);
-        return new Promise((resolve) => {
-            setTimeout(() => {
-                resolve({ code: 200, msg: '导入成功' });
-            }, 1000);
-        });
+        return importTurnover(file);
     }
 
     /** 提交 */
@@ -98,13 +102,13 @@
         importBooks(file.raw)
             .then((res) => {
                 loading.value = false;
-                ElMessage.success(res.msg);
+                ElMessage.success(actionMessage(res, '导入成功'));
                 visible.value = false;
                 emit('done');
             })
             .catch((e) => {
                 loading.value = false;
-                ElMessage.error('导入失败');
+                ElMessage.error(e.message || '导入失败');
             });
     };
 

+ 34 - 74
src/views/priceWorkbench/stockTurnoverRate/components/log-dialog.vue

@@ -1,97 +1,57 @@
 <template>
     <ele-modal
-        :width="800"
+        :width="860"
         title="操作记录"
         v-model="visible"
         :body-style="{ padding: '20px' }"
     >
-        <el-table :data="tableData" border style="width: 100%">
-            <el-table-column prop="importDate" label="导入日期" width="180" />
-            <el-table-column prop="operator" label="操作员" width="120" />
-            <el-table-column prop="calcPeriod" label="计算周期" width="120" />
-            <el-table-column
-                prop="turnoverRate"
-                label="库存周转率"
-                width="120"
-            />
-            <el-table-column
-                prop="remainingDays"
-                label="剩余可卖天数"
-                width="120"
-            />
-            <el-table-column prop="sales" label="销量" />
+        <el-table v-loading="loading" :data="tableData" border style="width: 100%">
+            <el-table-column prop="importTime" label="导入日期" min-width="170" />
+            <el-table-column prop="operatorName" label="操作员" width="120" />
+            <el-table-column prop="cycleName" label="计算周期" width="100" />
+            <el-table-column prop="turnoverRate" label="库存周转率" width="120" />
+            <el-table-column prop="remainSellDays" label="剩余可卖天数" width="120" />
+            <el-table-column prop="salesNum" label="销量" width="100" />
+            <el-table-column prop="avgStock" label="平均库存" width="100" />
         </el-table>
-        <div class="mt-4 flex justify-end">
-            <el-pagination
-                background
-                layout="total, sizes, prev, pager, next, jumper"
-                :total="total"
-                v-model:current-page="currentPage"
-                v-model:page-size="pageSize"
-                :page-sizes="[10, 20, 50, 100]"
-            />
-        </div>
         <template #footer>
-            <el-button type="primary" @click="visible = false">确定</el-button>
-            <el-button @click="visible = false">取消</el-button>
+            <el-button type="primary" @click="visible = false">关闭</el-button>
         </template>
     </ele-modal>
 </template>
 
 <script setup>
-    import { ref, defineExpose } from 'vue';
+    import { ref } from 'vue';
+    import { ElMessage } from 'element-plus';
+    import { getTurnoverHistory } from '@/api/priceWorkbench';
 
     const visible = defineModel({ type: Boolean });
-    const currentPage = ref(1);
-    const pageSize = ref(20);
-    const total = ref(100);
+    const loading = ref(false);
+    const tableData = ref([]);
+    const current = ref(null);
 
-    const tableData = ref([
-        {
-            importDate: '2025-06-15 15:00:00',
-            operator: 'xxx',
-            calcPeriod: '30天',
-            turnoverRate: '0.5',
-            remainingDays: '100',
-            sales: '200'
-        },
-        {
-            importDate: '2025-06-15 15:00:00',
-            operator: 'aaa',
-            calcPeriod: '30天',
-            turnoverRate: '0.5',
-            remainingDays: '100',
-            sales: '200'
-        },
-        {
-            importDate: '2025-06-15 15:00:00',
-            operator: 'ddd',
-            calcPeriod: '30天',
-            turnoverRate: '0.5',
-            remainingDays: '100',
-            sales: '200'
-        },
-        {
-            importDate: '2025-06-15 15:00:00',
-            operator: 'eee',
-            calcPeriod: '30天',
-            turnoverRate: '0.5',
-            remainingDays: '100',
-            sales: '200'
-        },
-        {
-            importDate: '2025-06-15 15:00:00',
-            operator: 'eee',
-            calcPeriod: '30天',
-            turnoverRate: '0.5',
-            remainingDays: '100',
-            sales: '200'
+    const loadData = async () => {
+        if (!current.value?.isbn) return;
+        loading.value = true;
+        try {
+            const res = await getTurnoverHistory({
+                isbn: current.value.isbn,
+                cycleType: current.value.cycleType
+            });
+            tableData.value = res.rows || res.data || [];
+        } catch (e) {
+            tableData.value = [];
+            ElMessage.error(e.message);
+        } finally {
+            loading.value = false;
         }
-    ]);
+    };
 
     const handleOpen = (row) => {
+        current.value = row;
+        tableData.value = [];
         visible.value = true;
-        // Fetch data for row
+        loadData();
     };
 
     defineExpose({ handleOpen });

+ 132 - 41
src/views/priceWorkbench/stockTurnoverRate/components/page-search.vue

@@ -6,7 +6,13 @@
             ref="searchRef"
             @search="search"
             :initKeys="initKeys"
-        />
+        >
+            <template #after-reset>
+                <el-button type="danger" plain class="ml-2" @click="emit('generate-rule')">
+                    生成规则
+                </el-button>
+            </template>
+        </ProSearch>
     </ele-card>
 </template>
 
@@ -15,7 +21,7 @@
     import ProSearch from '@/components/CommonPage/ProSearch2.vue';
 
     let { proxy } = getCurrentInstance();
-    const emit = defineEmits(['search']);
+    const emit = defineEmits(['search', 'generate-rule']);
 
     const formItems = reactive([
         { type: 'input', label: '书名', prop: 'bookName' },
@@ -46,24 +52,25 @@
         {
             type: 'select',
             label: '回收状态',
-            prop: 'recycleStatus',
+            prop: 'searchType',
             options: [
+                { label: '全部', value: 0 },
                 { label: '正在回收', value: 1 },
                 { label: '暂停回收', value: 2 },
-                { label: '未回收', value: 0 }
+                { label: '未加入回收', value: 3 }
             ]
         },
         {
             type: 'inputNumberRange',
             label: '销量',
             prop: 'sales',
-            keys: ['minSales', 'maxSales'],
+            keys: ['minSalesNum', 'maxSalesNum'],
             props: {
                 label: '销量',
                 onChange: (val) => {
                     searchRef.value?.setData({
-                        minSales: val.min,
-                        maxSales: val.max
+                        minSalesNum: val.min,
+                        maxSalesNum: val.max
                     });
                 }
             }
@@ -71,11 +78,10 @@
         {
             type: 'select',
             label: '计算周期',
-            prop: 'calcPeriod',
+            prop: 'cycleType',
             options: [
-                { label: '7天', value: 7 },
-                { label: '15天', value: 15 },
-                { label: '30天', value: 30 }
+                { label: '年', value: 1 },
+                { label: '半年', value: 2 }
             ]
         },
         {
@@ -97,13 +103,13 @@
             type: 'inputNumberRange',
             label: '剩余可卖天数',
             prop: 'remainingDays',
-            keys: ['minRemainingDays', 'maxRemainingDays'],
+            keys: ['minRemainSellDays', 'maxRemainSellDays'],
             props: {
                 label: '剩余可卖天数',
                 onChange: (val) => {
                     searchRef.value?.setData({
-                        minRemainingDays: val.min,
-                        maxRemainingDays: val.max
+                        minRemainSellDays: val.min,
+                        maxRemainSellDays: val.max
                     });
                 }
             }
@@ -112,13 +118,13 @@
             type: 'inputNumberRange',
             label: '库存',
             prop: 'stock',
-            keys: ['minStock', 'maxStock'],
+            keys: ['minStockNum', 'maxStockNum'],
             props: {
                 label: '库存',
                 onChange: (val) => {
                     searchRef.value?.setData({
-                        minStock: val.min,
-                        maxStock: val.max
+                        minStockNum: val.min,
+                        maxStockNum: val.max
                     });
                 }
             }
@@ -126,7 +132,7 @@
         {
             type: 'select',
             label: '单个订单多本回收',
-            prop: 'multiRecycle',
+            prop: 'multiPerOrder',
             options: [
                 { label: '是', value: 1 },
                 { label: '否', value: 0 }
@@ -153,16 +159,24 @@
             type: 'inputNumberRange',
             label: '价格',
             prop: 'price',
-            keys: ['minPrice', 'maxPrice'],
+            keys: ['minProductPrice', 'maxProductPrice'],
             props: {
                 label: '价格',
                 onChange: (val) => {
                     searchRef.value?.setData({
-                        minPrice: val.min,
-                        maxPrice: val.max
+                        minProductPrice: val.min,
+                        maxProductPrice: val.max
                     });
                 }
             }
+        },
+        {
+            type: 'input',
+            label: 'N天未改价',
+            prop: 'priceNotChangeDays',
+            props: {
+                placeholder: '如 60,表示60天内未更新售价'
+            }
         }
     ]);
 
@@ -172,34 +186,111 @@
         bookTag: '',
         minTurnoverRate: void 0,
         maxTurnoverRate: void 0,
-        recycleStatus: '',
-        minSales: void 0,
-        maxSales: void 0,
-        calcPeriod: '',
+        searchType: '',
+        minSalesNum: void 0,
+        maxSalesNum: void 0,
+        cycleType: '',
         minDiscount: void 0,
         maxDiscount: void 0,
-        minRemainingDays: void 0,
-        maxRemainingDays: void 0,
-        minStock: void 0,
-        maxStock: void 0,
-        multiRecycle: '',
+        minRemainSellDays: void 0,
+        maxRemainSellDays: void 0,
+        minStockNum: void 0,
+        maxStockNum: void 0,
+        multiPerOrder: '',
         priceChangeStartTime: '',
         priceChangeEndTime: '',
-        minPrice: void 0,
-        maxPrice: void 0
+        minProductPrice: void 0,
+        maxProductPrice: void 0,
+        priceNotChangeDays: ''
     });
 
     const searchRef = ref(null);
+
+    const UI_ONLY_KEYS = [
+        'turnoverRate',
+        'sales',
+        'calcPeriod',
+        'discount',
+        'remainingDays',
+        'stock',
+        'priceChangeTime',
+        'price',
+        'search',
+        'isReset'
+    ];
+
+    const RANGE_MAP = {
+        turnoverRate: ['minTurnoverRate', 'maxTurnoverRate'],
+        sales: ['minSalesNum', 'maxSalesNum'],
+        discount: ['minDiscount', 'maxDiscount'],
+        remainingDays: ['minRemainSellDays', 'maxRemainSellDays'],
+        stock: ['minStockNum', 'maxStockNum'],
+        price: ['minProductPrice', 'maxProductPrice']
+    };
+
+    function pickRange(value) {
+        if (Array.isArray(value)) {
+            return { min: value[0], max: value[1] };
+        }
+        if (value && typeof value === 'object') {
+            return { min: value.min, max: value.max };
+        }
+        return { min: undefined, max: undefined };
+    }
+
+    function normalizeCondition(data = {}) {
+        const params = JSON.parse(JSON.stringify(data || {}));
+
+        // 区间控件可能只写在复合字段上,这里拆到 min/max
+        Object.entries(RANGE_MAP).forEach(([uiKey, [minKey, maxKey]]) => {
+            if (params[uiKey] != null) {
+                const { min, max } = pickRange(params[uiKey]);
+                if (params[minKey] === undefined || params[minKey] === null || params[minKey] === '') {
+                    params[minKey] = min;
+                }
+                if (params[maxKey] === undefined || params[maxKey] === null || params[maxKey] === '') {
+                    params[maxKey] = max;
+                }
+            }
+        });
+
+        if (params.priceChangeTime && Array.isArray(params.priceChangeTime)) {
+            if (!params.priceChangeStartTime) {
+                params.priceChangeStartTime = params.priceChangeTime[0] || '';
+            }
+            if (!params.priceChangeEndTime) {
+                params.priceChangeEndTime = params.priceChangeTime[1] || '';
+            }
+        }
+
+        UI_ONLY_KEYS.forEach((key) => delete params[key]);
+
+        if (params.priceNotChangeDays !== '' && params.priceNotChangeDays != null) {
+            const n = Number(params.priceNotChangeDays);
+            params.priceNotChangeDays = Number.isNaN(n) ? undefined : n;
+        }
+
+        Object.keys(params).forEach((key) => {
+            if (params[key] === '' || params[key] === undefined || params[key] === null) {
+                delete params[key];
+            }
+        });
+        return params;
+    }
+
     /** 搜索 */
     const search = (data) => {
-        let params = JSON.parse(JSON.stringify(data));
-        delete params.turnoverRate;
-        delete params.sales;
-        delete params.discount;
-        delete params.remainingDays;
-        delete params.stock;
-        delete params.priceChangeTime;
-        delete params.price;
-        emit('search', params);
+        emit('search', { ...normalizeCondition(data), search: true });
     };
+
+    function getCondition() {
+        const raw = searchRef.value?.getData?.() || {};
+        return normalizeCondition(raw);
+    }
+
+    defineExpose({
+        getCondition,
+        search: () => searchRef.value?.search?.()
+    });
+
 </script>

+ 201 - 16
src/views/priceWorkbench/stockTurnoverRate/components/rule-dialog.vue

@@ -1,37 +1,222 @@
 <template>
     <ele-modal
-        :width="400"
+        :width="420"
         title="生成任务规则"
         v-model="visible"
         :body-style="{ padding: '20px' }"
     >
-        <div class="mb-4">
-            <div class="mb-2 text-base font-bold">1、60天没有更新价格</div>
-            <div class="mb-4 text-base font-bold">2、回收折扣0.5-0.6</div>
-            <el-checkbox
-                v-model="priority"
-                label="是否将其他规则任务中的数据优先处理"
-            />
+        <div v-if="descLines.length" class="rule-preview mb-4">
+            <div
+                v-for="(line, index) in descLines"
+                :key="index"
+                class="rule-line"
+            >
+                {{ index + 1 }}、{{ line }}
+            </div>
+        </div>
+        <el-empty
+            v-else
+            description="请先设置筛选条件后再生成规则"
+            :image-size="64"
+            class="mb-2"
+        />
+
+        <div class="mb-2">
+            <el-checkbox v-model="priority" :true-value="1" :false-value="0">
+                是否将其他规则任务中的数据优先处理
+            </el-checkbox>
         </div>
+
         <template #footer>
             <el-button @click="visible = false">取消</el-button>
-            <el-button type="success" @click="handleGenerate"
-                >生成规则</el-button
+            <el-button
+                type="success"
+                :loading="loading"
+                :disabled="!descLines.length"
+                @click="handleGenerate"
             >
+                生成规则
+            </el-button>
         </template>
     </ele-modal>
 </template>
 
 <script setup>
-    import { ref } from 'vue';
+    import { computed, nextTick, ref, watch } from 'vue';
     import { ElMessage } from 'element-plus';
+    import {
+        actionMessage,
+        addTaskRule,
+        cleanParams
+    } from '@/api/priceWorkbench';
 
     const visible = defineModel({ type: Boolean });
-    const priority = ref(true);
+    const emit = defineEmits(['done']);
 
-    const handleGenerate = () => {
-        // Mock generation
-        ElMessage.success('规则生成成功');
-        visible.value = false;
+    const props = defineProps({
+        condition: { type: Object, default: () => ({}) },
+        /** 打开弹窗时实时取筛选条件 */
+        getCondition: { type: Function, default: null }
+    });
+
+    const priority = ref(0);
+    const loading = ref(false);
+    const activeCondition = ref({});
+
+    const BOOK_TAG_MAP = {
+        1: '教材',
+        2: '社科',
+        3: '中小学',
+        4: '其他'
+    };
+    const SEARCH_TYPE_MAP = {
+        0: '全部回收状态',
+        1: '正在回收',
+        2: '暂停回收',
+        3: '未加入回收',
+        4: '黑名单',
+        5: '未加入回收(手动暂停)'
+    };
+    const CYCLE_MAP = { 1: '年', 2: '半年' };
+
+    const EXCLUDE_KEYS = new Set([
+        'offset',
+        'limitSize',
+        'pageNum',
+        'pageSize',
+        'page',
+        'limit',
+        'orderType',
+        'orderWay',
+        'search',
+        'isReset'
+    ]);
+
+    function hasValue(v) {
+        return !(v === '' || v === undefined || v === null);
+    }
+
+    function rangeText(label, min, max, unit = '') {
+        const hasMin = hasValue(min);
+        const hasMax = hasValue(max);
+        if (!hasMin && !hasMax) return '';
+        if (hasMin && hasMax) return `${label}${min}-${max}${unit}`;
+        if (hasMin) return `${label}大于等于${min}${unit}`;
+        return `${label}小于等于${max}${unit}`;
+    }
+
+    function buildConditionPayload(raw = {}) {
+        const cleaned = cleanParams(raw);
+        EXCLUDE_KEYS.forEach((key) => delete cleaned[key]);
+        return cleaned;
+    }
+
+    function buildDescLines(condition = {}) {
+        const c = condition || {};
+        const lines = [];
+
+        if (hasValue(c.priceNotChangeDays)) {
+            lines.push(`${c.priceNotChangeDays}天没有更新过售价`);
+        }
+        const discount = rangeText('回收折扣', c.minDiscount, c.maxDiscount, '折');
+        if (discount) lines.push(discount);
+
+        const price = rangeText('售价', c.minProductPrice, c.maxProductPrice);
+        if (price) lines.push(price);
+
+        const turnover = rangeText(
+            '库存周转率',
+            c.minTurnoverRate,
+            c.maxTurnoverRate
+        );
+        if (turnover) lines.push(turnover);
+
+        const remain = rangeText(
+            '剩余可卖天数',
+            c.minRemainSellDays,
+            c.maxRemainSellDays
+        );
+        if (remain) lines.push(remain);
+
+        const sales = rangeText('销量', c.minSalesNum, c.maxSalesNum);
+        if (sales) lines.push(sales);
+
+        const stock = rangeText('当前库存', c.minStockNum, c.maxStockNum);
+        if (stock) lines.push(stock);
+
+        if (hasValue(c.bookTag)) {
+            lines.push(`图书类型${BOOK_TAG_MAP[c.bookTag] || c.bookTag}`);
+        }
+        if (hasValue(c.searchType) && Number(c.searchType) !== 0) {
+            lines.push(SEARCH_TYPE_MAP[c.searchType] || `回收状态${c.searchType}`);
+        }
+        if (hasValue(c.cycleType)) {
+            lines.push(`计算周期${CYCLE_MAP[c.cycleType] || c.cycleType}`);
+        }
+        if (Number(c.multiPerOrder) === 1) {
+            lines.push('单个订单多本回收');
+        }
+        if (c.priceChangeStartTime || c.priceChangeEndTime) {
+            lines.push(
+                `售价变动时间${c.priceChangeStartTime || ''}~${c.priceChangeEndTime || ''}`
+            );
+        }
+        if (c.bookName) lines.push(`书名含「${c.bookName}」`);
+        if (c.isbn) lines.push(`ISBN=${c.isbn}`);
+
+        return lines;
+    }
+
+    const descLines = computed(() => buildDescLines(activeCondition.value));
+
+    function syncCondition() {
+        const raw =
+            typeof props.getCondition === 'function'
+                ? props.getCondition()
+                : props.condition;
+        activeCondition.value = buildConditionPayload(raw || {});
+    }
+
+    watch(visible, async (v) => {
+        if (v) {
+            priority.value = 0;
+            await nextTick();
+            syncCondition();
+        }
+    });
+
+    const handleGenerate = async () => {
+        syncCondition();
+        const condition = activeCondition.value;
+        if (!Object.keys(condition).length) {
+            ElMessage.warning('请先设置筛选条件后再生成规则');
+            return;
+        }
+        loading.value = true;
+        try {
+            const res = await addTaskRule({
+                condition,
+                priorityOther: Number(priority.value) || 0
+            });
+            ElMessage.success(actionMessage(res, '规则生成成功'));
+            visible.value = false;
+            emit('done', res);
+        } catch (e) {
+            ElMessage.error(e.message);
+        } finally {
+            loading.value = false;
+        }
     };
 </script>
+
+<style scoped>
+    .rule-preview {
+        line-height: 1.9;
+    }
+
+    .rule-line {
+        font-size: 15px;
+        font-weight: 600;
+        color: var(--el-text-color-primary);
+    }
+</style>

+ 95 - 76
src/views/priceWorkbench/stockTurnoverRate/index.vue

@@ -1,6 +1,6 @@
 <template>
     <ele-page flex-table>
-        <page-search @search="reload" />
+        <page-search ref="searchRef" @search="reload" @generate-rule="openRuleDialog" />
 
         <common-table
             ref="pageRef"
@@ -14,10 +14,6 @@
                     <el-icon class="mr-1"><Upload /></el-icon>
                     批量导入
                 </el-button>
-                <el-button type="danger" plain @click="ruleVisible = true">
-                    <el-icon class="mr-1"><DocumentAdd /></el-icon>
-                    生成规则
-                </el-button>
             </template>
 
             <template #cover="{ row }">
@@ -33,54 +29,53 @@
 
             <template #info="{ row }">
                 <div class="flex flex-col text-sm">
-                    <div class="mb-1">
-                        <span class="font-bold">书名: </span>{{ row.bookName }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">作者: </span>{{ row.author }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">ISBN: </span>{{ row.isbn }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">出版社: </span
-                        >{{ row.publisher }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">出版时间: </span
-                        >{{ row.pubDate }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">所属类型: </span>{{ row.type }}
-                    </div>
-                    <div class="mb-1">
-                        <span class="font-bold">回收状态: </span
-                        >{{ row.recycleStatus }}
+                    <div class="info-basic-grid">
+                        <div>
+                            <span class="font-bold">书名: </span>{{ row.bookName }}
+                        </div>
+                        <div>
+                            <span class="font-bold">作者: </span>{{ row.author }}
+                        </div>
+                        <div>
+                            <span class="font-bold">ISBN: </span>{{ row.isbn }}
+                        </div>
+                        <div>
+                            <span class="font-bold">出版社: </span>{{ row.publish }}
+                        </div>
+                        <div>
+                            <span class="font-bold">出版时间: </span>{{ row.pubDate }}
+                        </div>
+                        <div>
+                            <span class="font-bold">所属类型: </span>{{ row.bookTagName }}
+                        </div>
+                        <div>
+                            <span class="font-bold">回收状态: </span>{{ row.recycleStatusName }}
+                        </div>
                     </div>
                     <div class="mt-2 border-t pt-2">
                         <span class="mr-4">定价: {{ row.price }}</span>
                         <span class="mr-4"
                             >回收折扣: {{ row.recycleDiscount }}折
                             <span class="text-red-500 text-xs"
-                                >建议0.5-0.6折</span
+                                >{{ row.sugDiscountStr || '建议折扣' }}</span
                             ></span
                         >
                         <span>回收价格: {{ row.recyclePrice }}</span>
                     </div>
                     <div class="mb-1">
                         <span class="mr-4"
-                            >销售价格: {{ row.salesPrice }}
+                            >销售价格: {{ row.productPrice }}
                             <el-icon class="cursor-pointer text-blue-500"
                                 ><Edit /></el-icon
                         ></span>
                         <span class="text-green-500 text-xs"
-                            >谢程娟: 2025-06-15</span
+                            >{{ row.productPriceChangeLog?.operatorName || '' }} {{ row.productPriceChangeLog?.createTime || '' }}</span
                         >
                     </div>
                     <div class="mb-1">
                         <span
-                            >(已回收数量: {{ row.recycledCount }} 当前库存:
-                            {{ row.currentStock }})</span
+                            >(已回收数量: {{ row.recycleNum }} 当前库存:
+                            {{ row.stockNum }})</span
                         >
                     </div>
                 </div>
@@ -89,11 +84,11 @@
             <template #turnoverRate="{ row }">
                 <div class="flex items-center">
                     <el-tag
-                        :type="row.turnoverType === '年' ? 'primary' : 'danger'"
+                        :type="row.cycleName === '年' ? 'primary' : 'danger'"
                         effect="plain"
                         class="mr-1"
                     >
-                        {{ row.turnoverType }}
+                        {{ row.cycleName }}
                     </el-tag>
                     <span>{{ row.turnoverRate }}</span>
                     <el-icon
@@ -118,7 +113,12 @@
         </common-table>
 
         <import-dialog v-model="importVisible" @done="reload" />
-        <rule-dialog v-model="ruleVisible" />
+        <rule-dialog
+            v-model="ruleVisible"
+            :condition="lastWhere"
+            :get-condition="getRuleCondition"
+            @done="handleRuleDone"
+        />
         <history-dialog v-model="historyVisible" ref="historyRef" />
         <log-dialog v-model="logVisible" ref="logRef" />
     </ele-page>
@@ -126,9 +126,10 @@
 
 <script setup>
     import { ref, reactive } from 'vue';
+    import request from '@/utils/request';
+    import { cleanParams, withOffsetLimit } from '@/api/priceWorkbench';
     import {
         Upload,
-        DocumentAdd,
         Edit,
         DataLine
     } from '@element-plus/icons-vue';
@@ -143,6 +144,7 @@
 
     // Component Refs
     const pageRef = ref(null);
+    const searchRef = ref(null);
     const historyRef = ref(null);
     const logRef = ref(null);
 
@@ -155,24 +157,24 @@
     const columns = ref([
         { type: 'selection', width: 50, align: 'center', fixed: 'left' },
         { label: '图片', prop: 'cover', width: 100, slot: 'cover' },
-        { label: '信息', prop: 'info', minWidth: 400, slot: 'info' },
+        { label: '信息', prop: 'info', minWidth: 300, slot: 'info' },
         {
             label: '库存周转率',
             prop: 'turnoverRate',
-            width: 150,
+            width: 140,
             slot: 'turnoverRate',
             sortable: true
         },
         {
             label: '剩余可卖天数',
-            prop: 'remainingDays',
-            width: 120,
+            prop: 'remainSellDays',
+            width: 130,
             sortable: true
         },
-        { label: '销量', prop: 'sales', width: 100, sortable: true },
-        { label: '期初库存', prop: 'initialStock', width: 100, sortable: true },
-        { label: '期末库存', prop: 'endingStock', width: 100, sortable: true },
-        { label: '平均库存', prop: 'averageStock', width: 100, sortable: true },
+        { label: '销量', prop: 'turnoverSalesNum', width: 100, sortable: true },
+        { label: '期初库存', prop: 'beginStock', width: 100, sortable: true },
+        { label: '期末库存', prop: 'endStock', width: 100, sortable: true },
+        { label: '平均库存', prop: 'avgStock', width: 100, sortable: true },
         {
             label: '操作',
             prop: 'action',
@@ -182,50 +184,58 @@
         }
     ]);
 
+    const lastWhere = ref({});
+
     const pageConfig = reactive({
         fileName: '库存周转率',
         cacheKey: 'stockTurnoverTable',
         params: {},
-        rowKey: 'id'
+        rowKey: 'bookId'
     });
 
-    // Mock datasource
-    const datasource = ({ page, limit, where, orders }) => {
-        return Promise.resolve({
-            code: 0,
-            msg: 'success',
-            count: 10,
-            data: Array.from({ length: 10 }).map((_, i) => ({
-                id: i + 1,
-                cover: 'https://img1.baidu.com/it/u=2356551695,3062334057&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=707',
-                bookName: '马克思主义基本原理',
-                author: '编写组',
-                isbn: '9787056025415',
-                publisher: '高等教育出版社',
-                pubDate: '2018-06',
-                type: '教材库',
-                recycleStatus: '正在回收',
-                price: 25,
-                recycleDiscount: 0.2,
-                recyclePrice: 0.5,
-                salesPrice: 6.5,
-                recycledCount: 200,
-                currentStock: 75,
-                turnoverRate: 200,
-                turnoverType: i % 2 === 0 ? '年' : '半',
-                remainingDays: 55,
-                sales: 200,
-                initialStock: 200,
-                endingStock: 200,
-                averageStock: 50
-            }))
+    const datasource = ({ pages, where, orders, sorter }) => {
+        lastWhere.value = { ...(where || {}) };
+        const params = withOffsetLimit(pages, {
+            ...(pageConfig.params || {}),
+            ...cleanParams(where || {})
         });
+        if (orders && orders.orderByColumn) {
+            params.orderType = sorter?.column?.columnKey || orders.orderByColumn;
+            params.orderWay = orders.isAsc === 'ascending' ? 'asc' : 'desc';
+        }
+        return request
+            .get('/book/bookTurnover/list', { params })
+            .then((res) => {
+                if (res.data.code === 200) return res.data;
+                return Promise.reject(new Error(res.data.msg));
+            });
     };
 
     function reload(where) {
+        if (where) lastWhere.value = { ...where };
         pageRef.value?.reload(where);
     }
 
+    
+    function getRuleCondition() {
+        const live = searchRef.value?.getCondition?.();
+        if (live && Object.keys(live).length) {
+            return live;
+        }
+        return lastWhere.value || {};
+    }
+
+    function handleRuleDone() {
+        pageRef.value?.reload();
+    }
+
+    function openRuleDialog() {
+        // 打开前先同步一次当前筛选,避免弹窗读到空条件
+        const live = getRuleCondition();
+        lastWhere.value = { ...(live || {}) };
+        ruleVisible.value = true;
+    }
+
     function handleHistory(row) {
         historyVisible.value = true;
         historyRef.value?.setData(row);
@@ -236,3 +246,12 @@
         logRef.value?.handleOpen(row);
     }
 </script>
+
+<style scoped>
+    .info-basic-grid {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        column-gap: 16px;
+        row-gap: 4px;
+    }
+</style>

+ 42 - 14
src/views/priceWorkbench/taskList/components/assign-dialog.vue

@@ -1,5 +1,5 @@
 <template>
-    <ele-modal v-model="visible" title="分配任务" width="500px">
+    <ele-modal v-model="visible" title="分配任务" width="640px">
         <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
             <el-form-item label="生成数量" prop="quantity">
                 <el-input v-model="form.quantity" disabled />
@@ -21,9 +21,9 @@
                     placeholder="请选择指派人"
                     style="width: 100%"
                 >
-                    <el-option label="张三" value="zhangsan" />
-                    <el-option label="李四" value="lisi" />
-                    <el-option label="王五" value="wangwu" />
+                    <el-option v-for="user in handlers" :key="user.userId" :label="user.nickName" :value="user.userId" />
+                    <!-- handler -->
+                    <!-- end -->
                 </el-select>
             </el-form-item>
             <el-form-item label="计划完成时间" prop="scheduledTime">
@@ -38,7 +38,7 @@
         </el-form>
         <template #footer>
             <el-button @click="visible = false">取消</el-button>
-            <el-button type="primary" @click="handleSubmit">确定</el-button>
+            <el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
         </template>
     </ele-modal>
 </template>
@@ -46,11 +46,17 @@
 <script setup>
     import { ref, reactive } from 'vue';
     import { QuestionFilled } from '@element-plus/icons-vue';
-    import request from '@/utils/request';
     import { ElMessage } from 'element-plus';
+    import {
+        actionMessage,
+        assignTask,
+        getHandlerOptions
+    } from '@/api/priceWorkbench';
 
     const emit = defineEmits(['success']);
     const visible = ref(false);
+    const handlers = ref([]);
+    const submitting = ref(false);
     const formRef = ref();
     const form = reactive({
         id: null,
@@ -70,21 +76,43 @@
 
     const handleOpen = (row) => {
         form.id = row.id;
-        form.quantity = row.quantity || 2000; // Mock data or from row
+        form.quantity = row.unassignedNum || 0;
         form.assignee = '';
         form.scheduledTime = '';
         visible.value = true;
+        getHandlerOptions()
+            .then((res) => {
+                handlers.value = res.data || [];
+            })
+            .catch(() => {
+                handlers.value = [];
+            });
     };
 
     const handleSubmit = async () => {
         await formRef.value.validate();
-        // TODO: Replace with actual API endpoint
-        // const res = await request.post('/priceWorkbench/task/assign', form);
-        // if (res.data.code === 200) {
-        ElMessage.success('分配成功');
-        visible.value = false;
-        emit('success');
-        // }
+        submitting.value = true;
+        try {
+            const res = await assignTask({
+                taskId: form.id,
+                planFinishTime: form.scheduledTime
+                    ? form.scheduledTime + ' 23:59:59'
+                    : '',
+                handlers: [
+                    {
+                        userId: form.assignee,
+                        num: Number(form.quantity) || 1
+                    }
+                ]
+            });
+            ElMessage.success(actionMessage(res, '分配成功'));
+            visible.value = false;
+            emit('success');
+        } catch (e) {
+            ElMessage.error(e.message);
+        } finally {
+            submitting.value = false;
+        }
     };
 
     defineExpose({ handleOpen });

+ 64 - 55
src/views/priceWorkbench/taskList/components/details-dialog.vue

@@ -1,33 +1,39 @@
 <template>
-    <ele-modal v-model="visible" title="查看详情" width="1000px">
-        <el-table :data="tableData" border stripe>
-            <el-table-column prop="subTaskId" label="任务ID" width="100" />
-            <el-table-column prop="handler" label="处理人" />
-            <el-table-column prop="quantity" label="任务数量" />
-            <el-table-column prop="completedQuantity" label="已完成数量" />
-            <el-table-column prop="progress" label="完成进度" />
+    <ele-modal v-model="visible" title="查看详情" width="1100px">
+        <el-table v-loading="loading" :data="tableData" border stripe>
+            <el-table-column prop="id" label="子任务ID" width="100" />
+            <el-table-column prop="handlerName" label="处理人" min-width="100" />
+            <el-table-column prop="totalNum" label="任务数量" width="100" />
+            <el-table-column prop="doneNum" label="已完成数量" width="110" />
+            <el-table-column prop="progress" label="完成进度" width="100" />
+            <el-table-column label="状态" width="90">
+                <template #default="{ row }">
+                    <el-tag :type="row.status === 2 ? 'info' : 'success'" effect="plain">
+                        {{ row.status === 2 ? '已撤销' : '正常' }}
+                    </el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column prop="createTime" label="分配时间" min-width="160" />
             <el-table-column
-                prop="scheduledTime"
+                prop="planFinishTime"
                 label="计划完成时间"
-                width="120"
+                min-width="160"
             />
-            <el-table-column
-                prop="completionTime"
-                label="完成时间"
-                width="160"
-            />
-            <el-table-column label="操作" width="100" fixed="right">
+            <el-table-column prop="finishTime" label="完成时间" min-width="160" />
+            <el-table-column label="操作" width="110" fixed="right">
                 <template #default="{ row }">
                     <el-popconfirm
-                        title="确定要撤销任务吗?撤销后无法重启!"
+                        title="确定要撤销该子任务吗?未处理图书会退回待分配。"
                         @confirm="handleRevoke(row)"
                     >
                         <template #reference>
-                            <el-button link type="primary">
-                                [撤销任务]
-                                <el-icon class="el-icon--right"
-                                    ><Lightning
-                                /></el-icon>
+                            <el-button
+                                link
+                                type="danger"
+                                v-permission="'price:task:revokeSub'"
+                                :disabled="row.status !== 1 || revokingId === row.id"
+                            >
+                                撤销任务
                             </el-button>
                         </template>
                     </el-popconfirm>
@@ -42,49 +48,52 @@
 
 <script setup>
     import { ref } from 'vue';
-    import { Lightning } from '@element-plus/icons-vue';
     import { ElMessage } from 'element-plus';
+    import {
+        actionMessage,
+        getTaskSubList,
+        revokeSubTask
+    } from '@/api/priceWorkbench';
 
+    const emit = defineEmits(['success']);
     const visible = ref(false);
+    const loading = ref(false);
+    const revokingId = ref(null);
+    const taskId = ref(null);
     const tableData = ref([]);
 
+    const loadList = async () => {
+        loading.value = true;
+        try {
+            const res = await getTaskSubList(taskId.value);
+            tableData.value = res.data || [];
+        } catch (e) {
+            tableData.value = [];
+            ElMessage.error(e.message);
+        } finally {
+            loading.value = false;
+        }
+    };
+
     const handleOpen = (row) => {
-        // Mock data based on row
-        tableData.value = [
-            {
-                subTaskId: '565632',
-                handler: 'zzz',
-                quantity: 500,
-                completedQuantity: 300,
-                progress: '60%',
-                scheduledTime: '2025-06-15',
-                completionTime: ''
-            },
-            {
-                subTaskId: '565632',
-                handler: 'zzz',
-                quantity: 500,
-                completedQuantity: 300,
-                progress: '60%',
-                scheduledTime: '2025-06-15',
-                completionTime: ''
-            },
-            {
-                subTaskId: '565632',
-                handler: 'zzz',
-                quantity: 500,
-                completedQuantity: 500,
-                progress: '100%',
-                scheduledTime: '2025-06-15',
-                completionTime: '2025-06-15 15:00:00'
-            }
-        ];
+        taskId.value = row.id;
+        tableData.value = [];
         visible.value = true;
+        loadList();
     };
 
-    const handleRevoke = (row) => {
-        // Logic to revoke sub-task
-        ElMessage.success('撤销成功');
+    const handleRevoke = async (row) => {
+        revokingId.value = row.id;
+        try {
+            const res = await revokeSubTask(row.id);
+            ElMessage.success(actionMessage(res, '撤销成功'));
+            await loadList();
+            emit('success');
+        } catch (e) {
+            ElMessage.error(e.message);
+        } finally {
+            revokingId.value = null;
+        }
     };
 
     defineExpose({ handleOpen });

+ 60 - 39
src/views/priceWorkbench/taskList/components/log-dialog.vue

@@ -1,58 +1,79 @@
 <template>
-    <ele-modal v-model="visible" title="操作日志" width="800px">
-        <el-table :data="tableData" border stripe>
-            <el-table-column prop="operator" label="操作员" width="120" />
-            <el-table-column
-                prop="operationTime"
-                label="操作时间"
-                width="180"
-            />
-            <el-table-column prop="description" label="描述" />
+    <ele-modal v-model="visible" title="操作日志" width="860px">
+        <el-table v-loading="loading" :data="tableData" border stripe>
+            <el-table-column prop="operatorName" label="操作员" width="140" />
+            <el-table-column prop="createTime" label="操作时间" width="180" />
+            <el-table-column prop="content" label="描述" min-width="280" />
         </el-table>
+        <div class="pager">
+            <el-pagination
+                background
+                layout="total, sizes, prev, pager, next"
+                :total="total"
+                v-model:current-page="pageNum"
+                v-model:page-size="pageSize"
+                :page-sizes="[10, 20, 50]"
+                @current-change="loadList"
+                @size-change="handleSizeChange"
+            />
+        </div>
         <template #footer>
-            <el-button type="primary" @click="visible = false">确定</el-button>
-            <el-button @click="visible = false">取消</el-button>
+            <el-button type="primary" @click="visible = false">关闭</el-button>
         </template>
     </ele-modal>
 </template>
 
 <script setup>
     import { ref } from 'vue';
+    import { ElMessage } from 'element-plus';
+    import { getTaskLogs } from '@/api/priceWorkbench';
 
     const visible = ref(false);
+    const loading = ref(false);
     const tableData = ref([]);
+    const total = ref(0);
+    const pageNum = ref(1);
+    const pageSize = ref(10);
+    const taskId = ref(null);
+
+    const loadList = async () => {
+        if (!taskId.value) return;
+        loading.value = true;
+        try {
+            const res = await getTaskLogs(taskId.value, {
+                pageNum: pageNum.value,
+                pageSize: pageSize.value
+            });
+            tableData.value = res.rows || [];
+            total.value = res.total || 0;
+        } catch (e) {
+            tableData.value = [];
+            total.value = 0;
+            ElMessage.error(e.message);
+        } finally {
+            loading.value = false;
+        }
+    };
+
+    const handleSizeChange = () => {
+        pageNum.value = 1;
+        loadList();
+    };
 
     const handleOpen = (row) => {
-        // Mock data
-        tableData.value = [
-            {
-                operator: 'zzz',
-                operationTime: '2025-06-15 15:00:00',
-                description: '生成任务'
-            },
-            {
-                operator: 'aaa',
-                operationTime: '2025-06-15 15:00:00',
-                description: '分配任务给aaa、bbb、ccc,计划完成时间2026-01-10'
-            },
-            {
-                operator: 'ddd',
-                operationTime: '2025-06-15 15:00:00',
-                description: '完成子任务'
-            },
-            {
-                operator: 'aaa',
-                operationTime: '2025-06-15 15:00:00',
-                description: '完成子任务'
-            },
-            {
-                operator: 'eee',
-                operationTime: '2025-06-15 15:00:00',
-                description: '完成子任务'
-            }
-        ];
+        taskId.value = row.id;
+        pageNum.value = 1;
         visible.value = true;
+        loadList();
     };
 
     defineExpose({ handleOpen });
 </script>
+
+<style scoped>
+    .pager {
+        display: flex;
+        justify-content: flex-end;
+        margin-top: 12px;
+    }
+</style>

+ 40 - 20
src/views/priceWorkbench/taskList/components/page-search.vue

@@ -5,30 +5,50 @@
 </template>
 
 <script setup>
-    import { computed, defineEmits } from 'vue';
+    import { computed, onMounted, ref } from 'vue';
     import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+    import { listAdmins } from '@/api/activation/repository';
+    import { cleanParams } from '@/api/priceWorkbench';
 
     const emit = defineEmits(['search']);
+    const admins = ref([]);
 
-    const formItems = computed(() => {
-        return [
-            { type: 'input', label: '书名', prop: 'bookName' },
-            { type: 'input', label: 'ISBN', prop: 'isbn' },
-            {
-                type: 'select',
-                label: '任务状态',
-                prop: 'status',
-                options: [
-                    { label: '待分配', value: 1 },
-                    { label: '待作业', value: 2 },
-                    { label: '作业中', value: 3 },
-                    { label: '已完成', value: 4 },
-                    { label: '已撤销', value: 5 }
-                ]
-            },
-            { type: 'input', label: '操作员', prop: 'operator' }
-        ];
+    const formItems = computed(() => [
+        { type: 'input', label: '书名', prop: 'bookName' },
+        { type: 'input', label: 'ISBN', prop: 'isbn' },
+        {
+            type: 'select',
+            label: '任务状态',
+            prop: 'status',
+            props: { filterable: true },
+            options: [
+                { label: '待分配', value: 1 },
+                { label: '待作业', value: 2 },
+                { label: '作业中', value: 3 },
+                { label: '已完成', value: 4 },
+                { label: '已撤销', value: 5 }
+            ]
+        },
+        {
+            type: 'select',
+            label: '操作员',
+            prop: 'createId',
+            props: { filterable: true },
+            options: admins.value.map((item) => ({
+                label: item.nickName || item.userName || item.userId,
+                value: item.userId
+            }))
+        }
+    ]);
+
+    onMounted(async () => {
+        try {
+            const list = await listAdmins();
+            admins.value = Array.isArray(list) ? list : [];
+        } catch (e) {
+            admins.value = [];
+        }
     });
 
-    const search = (data) => emit('search', data);
+    const search = (data) => emit('search', { ...cleanParams(data), search: true });
 </script>

+ 72 - 33
src/views/priceWorkbench/taskList/index.vue

@@ -4,9 +4,9 @@
         <page-search @search="reload" />
 
         <!-- Stats Cards -->
-        <div class="stats-container mb-4">
-            <el-row :gutter="20">
-                <el-col :span="6" v-for="(stat, index) in stats" :key="index">
+        <div class="stats-container">
+            <el-row :gutter="16">
+                <el-col :span="8" v-for="(stat, index) in stats" :key="index">
                     <div class="stat-card">
                         <div class="stat-value">{{ stat.value }}</div>
                         <div class="stat-label">
@@ -45,13 +45,13 @@
                     <el-button link type="primary" @click="handleAssign(row)">
                         [分配任务]
                         <el-icon class="el-icon--right">
-                            <Lightning />
+                            <span />
                         </el-icon>
                     </el-button>
                     <el-button link type="primary" @click="handleView(row)">
                         [查看详情]
                         <el-icon class="el-icon--right">
-                            <Lightning />
+                            <span />
                         </el-icon>
                     </el-button>
                     <el-popconfirm
@@ -63,16 +63,17 @@
                                 <el-tooltip
                                     content="只有待分配的任务可以撤销"
                                     placement="top"
-                                    :disabled="row.status === '待分配'"
+                                    :disabled="row.status === 1"
                                 >
                                     <el-button
                                         link
                                         type="primary"
-                                        :disabled="row.status !== '待分配'"
+                                        :disabled="row.status !== 1"
+                                        :loading="revokingId === row.id"
                                     >
                                         [撤销任务]
                                         <el-icon class="el-icon--right">
-                                            <Lightning />
+                                            <span />
                                         </el-icon>
                                     </el-button>
                                 </el-tooltip>
@@ -82,7 +83,7 @@
                     <el-button link type="primary" @click="handleLog(row)">
                         [操作日志]
                         <el-icon class="el-icon--right">
-                            <Lightning />
+                            <span />
                         </el-icon>
                     </el-button>
                 </div>
@@ -90,50 +91,55 @@
         </common-table>
 
         <!-- Dialogs -->
-        <assign-dialog ref="assignDialogRef" @success="reload" />
-        <details-dialog ref="detailsDialogRef" />
+        <assign-dialog ref="assignDialogRef" @success="handleSuccess" />
+        <details-dialog ref="detailsDialogRef" @success="handleSuccess" />
         <log-dialog ref="logDialogRef" />
     </ele-page>
 </template>
 
 <script setup>
-    import { ref, reactive } from 'vue';
-    import { QuestionFilled, Lightning } from '@element-plus/icons-vue';
+    import { onMounted, ref, reactive } from 'vue';
+    import { QuestionFilled } from '@element-plus/icons-vue';
     import CommonTable from '@/components/CommonPage/CommonTable.vue';
     import PageSearch from './components/page-search.vue';
     import AssignDialog from './components/assign-dialog.vue';
     import DetailsDialog from './components/details-dialog.vue';
     import LogDialog from './components/log-dialog.vue';
     import { ElMessage } from 'element-plus';
+    import {
+        actionMessage,
+        getTaskStat,
+        revokeTask
+    } from '@/api/priceWorkbench';
 
-    defineOptions({ name: 'TaskList' });
+    defineOptions({ name: 'PriceTaskList' });
 
-    // Stats Data
     const stats = ref([
-        { label: '待分配任务', value: 56666 },
-        { label: '待作业任务', value: 56666 },
-        { label: '总任务进度', value: '60%', tooltip: '已完成的任务不计算在内' }
+        { label: '待分配任务', value: '-' },
+        { label: '待作业任务', value: '-' },
+        { label: '总任务进度', value: '-', tooltip: '已完成、已撤销的任务不计算在内' }
     ]);
 
     // Table Config
     const pageConfig = reactive({
-        pageUrl: '/priceWorkbench/task/list', // Replace with actual API
+        pageUrl: '/price/task/list',
         rowKey: 'id',
-        mock: true // Enable mock if no backend
+        fileName: '价格任务',
+        cacheKey: 'priceTaskList'
     });
 
     const columns = ref([
-        { label: '任务编号', prop: 'taskNo', minWidth: 100 },
-        { label: '任务数量', prop: 'quantity', minWidth: 100 },
-        { label: '任务状态', prop: 'status', minWidth: 100 },
+        { label: '任务编号', prop: 'id', minWidth: 100 },
+        { label: '任务数量', prop: 'totalNum', minWidth: 100 },
+        { label: '任务状态', prop: 'statusName', minWidth: 100 },
         {
             label: '任务描述',
-            prop: 'description',
+            prop: 'taskDesc',
             minWidth: 200,
             showOverflowTooltip: true
         },
-        { label: '任务生成时间', prop: 'createTime', minWidth: 160 },
-        { label: '操作员', prop: 'operator', slot: 'operator', minWidth: 100 },
+        { label: '任务生成时间', prop: 'createTime', minWidth: 170 },
+        { label: '操作员', prop: 'createName', minWidth: 100 },
         { label: '操作', slot: 'action', fixed: 'right', width: 340 }
     ]);
 
@@ -143,8 +149,32 @@
     const detailsDialogRef = ref(null);
     const logDialogRef = ref(null);
 
-    // Actions
-    const reload = (params) => pageRef.value?.reload(params);
+    const revokingId = ref(null);
+
+    const loadStat = async () => {
+        try {
+            const res = await getTaskStat();
+            const data = res.data || {};
+            stats.value = [
+                { label: '待分配任务', value: data.waitAssignTaskNum ?? '-' },
+                { label: '待作业任务', value: data.waitWorkTaskNum ?? '-' },
+                {
+                    label: '总任务进度',
+                    value: data.progress ?? '-',
+                    tooltip: '已完成、已撤销的任务不计算在内'
+                }
+            ];
+        } catch (e) {
+            // ignore
+        }
+    };
+
+    const reload = (params) => {
+        pageRef.value?.reload(params);
+        loadStat();
+    };
+
+    const handleSuccess = () => reload();
 
     const handleAssign = (row) => {
         assignDialogRef.value?.handleOpen(row);
@@ -154,21 +184,30 @@
         detailsDialogRef.value?.handleOpen(row);
     };
 
-    const handleRevoke = (row) => {
-        // API call to revoke
-        ElMessage.success('撤销成功');
-        reload();
+    const handleRevoke = async (row) => {
+        revokingId.value = row.id;
+        try {
+            const res = await revokeTask(row.id);
+            ElMessage.success(actionMessage(res, '撤销成功'));
+            reload();
+        } catch (e) {
+            ElMessage.error(e.message);
+        } finally {
+            revokingId.value = null;
+        }
     };
 
     const handleLog = (row) => {
         logDialogRef.value?.handleOpen(row);
     };
+
+    onMounted(loadStat);
 </script>
 
 <style lang="scss" scoped>
     .stats-container {
         .stat-card {
-            background-color: #f5f5f5;
+            background: var(--el-fill-color-light);
             padding: 20px;
             text-align: center;
             border-radius: 4px;

+ 162 - 1
src/views/priceWorkbench/taskPending/index.vue

@@ -1,3 +1,164 @@
 <template>
-    <div> 任务待处理 </div>
+    <ele-page flex-table>
+        <ele-card :body-style="{ paddingBottom: '8px' }">
+            <ProSearch :items="formItems" @search="reload" />
+        </ele-card>
+
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+            <template #action="{ row }">
+                <el-button link type="primary" @click="openProcess(row)">[去处理]</el-button>
+            </template>
+        </common-table>
+
+        <ele-modal
+            v-model="processVisible"
+            :title="processTitle"
+            width="1200px"
+        >
+            <el-radio-group v-model="itemTab" class="mb-3" @change="loadItems">
+                <el-radio-button :value="0">全部</el-radio-button>
+                <el-radio-button :value="1">待处理</el-radio-button>
+                <el-radio-button :value="2">已完成</el-radio-button>
+            </el-radio-group>
+            <el-table v-loading="itemLoading" :data="itemRows" border max-height="520">
+                <el-table-column label="封面" width="90">
+                    <template #default="{ row }">
+                        <el-image style="width:60px;height:80px" fit="cover" :src="row.cover" :preview-src-list="row.cover?[row.cover]:[]" preview-teleported />
+                    </template>
+                </el-table-column>
+                <el-table-column label="图书信息" min-width="260">
+                    <template #default="{ row }">
+                        <div class="text-sm">
+                            <div>书名:{{ row.bookName }}</div>
+                            <div>ISBN:{{ row.isbn }}</div>
+                            <div>作者:{{ row.author }}</div>
+                            <div>出版社:{{ row.publish }}</div>
+                            <div>定价:{{ row.price }} / 售价:{{ row.productPrice ?? row.oldPrice }}</div>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="itemStatusName" label="状态" width="100" />
+                <el-table-column label="新售价" width="150">
+                    <template #default="{ row }">
+                        <el-input-number v-model="row._newPrice" :min="0" :precision="2" :controls="false" :disabled="Number(row.itemStatus)===2 || itemTab===2" />
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" width="120" fixed="right">
+                    <template #default="{ row }">
+                        <el-button type="primary" size="small" :loading="submittingId===row.itemId" :disabled="Number(row.itemStatus)===2 || itemTab===2" @click="handleSubmit(row)">提交</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <div class="mt-3 flex justify-end">
+                <el-pagination background layout="total, prev, pager, next" :total="itemTotal" v-model:current-page="itemPage" v-model:page-size="itemPageSize" @current-change="loadItems" />
+            </div>
+        </ele-modal>
+    </ele-page>
 </template>
+
+<script setup>
+    import { ref, reactive } from 'vue';
+    import { ElMessage } from 'element-plus';
+    import CommonTable from '@/components/CommonPage/CommonTable.vue';
+    import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+    import {
+        actionMessage,
+        listMyTaskItems,
+        submitTaskPrice,
+        withOffsetLimit
+    } from '@/api/priceWorkbench';
+
+    defineOptions({ name: 'PriceTaskPending' });
+
+    const pageRef = ref(null);
+    const formItems = reactive([
+        { type: 'input', label: '书名', prop: 'bookName' },
+        { type: 'input', label: 'ISBN', prop: 'isbn' },
+        {
+            type: 'select',
+            label: '任务状态',
+            prop: 'status',
+            options: [
+                { label: '待作业', value: 2 },
+                { label: '作业中', value: 3 },
+                { label: '已完成', value: 4 }
+            ]
+        }
+    ]);
+
+    const pageConfig = reactive({
+        pageUrl: '/price/task/myList',
+        rowKey: 'id',
+        fileName: '我的价格任务',
+        cacheKey: 'priceTaskPending'
+    });
+
+    const columns = ref([
+        { label: '任务编号', prop: 'id', minWidth: 100 },
+        { label: '任务数量', prop: 'totalNum', minWidth: 100 },
+        { label: '已完成', prop: 'doneNum', minWidth: 100 },
+        { label: '进度', prop: 'progress', minWidth: 100 },
+        { label: '状态', prop: 'statusName', minWidth: 100 },
+        { label: '任务描述', prop: 'taskDesc', minWidth: 220, showOverflowTooltip: true },
+        { label: '生成时间', prop: 'createTime', minWidth: 170 },
+        { label: '操作', slot: 'action', fixed: 'right', width: 120 }
+    ]);
+
+    const processVisible = ref(false);
+    const processTitle = ref('去处理');
+    const currentTask = ref(null);
+    const itemTab = ref(1);
+    const itemLoading = ref(false);
+    const itemRows = ref([]);
+    const itemTotal = ref(0);
+    const itemPage = ref(1);
+    const itemPageSize = ref(10);
+    const submittingId = ref(null);
+
+    const reload = (params) => pageRef.value?.reload(params);
+
+    const openProcess = (row) => {
+        currentTask.value = row;
+        itemTab.value = 1;
+        itemPage.value = 1;
+        processTitle.value = `去处理 - 任务 #${row.id}`;
+        processVisible.value = true;
+        loadItems();
+    };
+
+    const loadItems = async () => {
+        if (!currentTask.value?.id) return;
+        itemLoading.value = true;
+        try {
+            const params = withOffsetLimit(
+                { pageNum: itemPage.value, pageSize: itemPageSize.value },
+                { taskId: currentTask.value.id, tab: itemTab.value }
+            );
+            const res = await listMyTaskItems(params);
+            itemTotal.value = res.total || 0;
+            itemRows.value = (res.rows || []).map((row) => ({
+                ...row,
+                _newPrice: row.newPrice ?? row.productPrice ?? row.oldPrice
+            }));
+        } catch (e) {
+            itemRows.value = [];
+            ElMessage.error(e.message);
+        } finally {
+            itemLoading.value = false;
+        }
+    };
+
+    const handleSubmit = async (row) => {
+        submittingId.value = row.itemId;
+        try {
+            const res = await submitTaskPrice({ itemId: row.itemId, newPrice: row._newPrice });
+            ElMessage.success(actionMessage(res, '提交成功'));
+            await loadItems();
+            reload();
+        } catch (e) {
+            ElMessage.error(e.message);
+        } finally {
+            submittingId.value = null;
+        }
+    };
+</script>

+ 179 - 139
src/views/priceWorkbench/taskSettings/index.vue

@@ -1,11 +1,12 @@
 <template>
-    <ele-page>
-        <div class="task-settings-container">
-            <el-row :gutter="20">
-                <el-col :span="8" v-for="rule in rules" :key="rule.id">
+    <ele-page class="task-settings-page">
+        <div class="task-settings-container" v-loading="loading">
+            <el-empty v-if="!loading && !rules.length" description="暂无任务规则" />
+            <el-row v-else :gutter="16">
+                <el-col :xs="24" :sm="24" :md="8" :lg="6" v-for="rule in rules" :key="rule.id" class="rule-col">
                     <div class="rule-card">
                         <div class="card-header">
-                            <span class="rule-title">{{ rule.title }}</span>
+                            <span class="rule-title">规则 #{{ rule.id }}</span>
                             <el-icon
                                 class="delete-icon"
                                 @click="handleDelete(rule)"
@@ -16,44 +17,43 @@
 
                         <div class="card-content">
                             <div class="rule-desc-section">
-                                <div class="label">规则描述:</div>
+                                <div class="label">规则描述</div>
                                 <div class="desc-list">
                                     <div
-                                        v-for="(
-                                            desc, index
-                                        ) in rule.description"
+                                        v-for="(desc, index) in (rule.ruleDescLines || [])"
                                         :key="index"
                                         class="desc-item"
                                     >
                                         {{ index + 1 }}、{{ desc }}
                                     </div>
+                                    <div
+                                        v-if="!(rule.ruleDescLines || []).length"
+                                        class="desc-item"
+                                    >
+                                        {{ rule.ruleDesc || '-' }}
+                                    </div>
                                 </div>
                             </div>
 
                             <div class="stats-text">
-                                {{ rule.stats }}
+                                约{{ rule.hitNum ?? 0 }}条({{ rule.newNum ?? 0 }}条新加入,{{ rule.processingNum ?? 0 }}条正在处理)
                             </div>
 
                             <div class="time-info">
-                                <div>规则新建时间:{{ rule.createTime }}</div>
-                                <div>规则更新时间:{{ rule.updateTime }}</div>
+                                <div>规则新建时间:{{ rule.createTime || "-" }}</div>
+                                <div>规则更新时间:{{ rule.updateTime || "-" }}</div>
                             </div>
 
                             <div class="action-area">
                                 <el-button
-                                    type="success"
-                                    v-if="rule.hasTask"
-                                    plain
-                                    class="action-btn"
-                                    >更新任务</el-button
-                                >
-                                <el-button
-                                    v-else
                                     type="success"
                                     plain
                                     class="action-btn"
-                                    >一键生成任务</el-button
+                                    :loading="generatingId === rule.id"
+                                    @click="handleGenerate(rule)"
                                 >
+                                    {{ rule.hasActiveTask ? "更新任务" : "一键生成任务" }}
+                                </el-button>
                             </div>
                         </div>
                     </div>
@@ -64,41 +64,34 @@
 </template>
 
 <script setup>
-    import { ref } from 'vue';
+    import { ref, onMounted } from 'vue';
     import { Delete } from '@element-plus/icons-vue';
     import { ElMessageBox, ElMessage } from 'element-plus';
+    import {
+        actionMessage,
+        deleteTaskRule,
+        generateTask,
+        listTaskRules
+    } from '@/api/priceWorkbench';
 
     defineOptions({ name: 'TaskSettings' });
 
-    const rules = ref([
-        {
-            id: 1,
-            title: '规则一',
-            description: ['60天没有更新过的售价', '销售价格大于4', '教材'],
-            stats: '约564555条 (5条新加入, 10条正在处理)',
-            createTime: '2025-06-15 15:00:00',
-            updateTime: '2025-06-15 15:00:00',
-            hasTask: true
-        },
-        {
-            id: 2,
-            title: '规则二',
-            description: ['库存周转率0.01', '库存大于10'],
-            stats: '约564555条 (5条新加入, 10条正在处理)',
-            createTime: '2025-06-15 15:00:00',
-            updateTime: '2025-06-15 15:00:00',
-            hasTask: false
-        },
-        {
-            id: 3,
-            title: '规则三',
-            description: ['回收折扣大于等于0.8', '多本回收'],
-            stats: '约564555条 (5条新加入, 10条正在处理)',
-            createTime: '2025-06-15 15:00:00',
-            updateTime: '2025-06-15 15:00:00',
-            hasTask: false
+    const loading = ref(false);
+    const rules = ref([]);
+    const generatingId = ref(null);
+
+    const loadRules = async () => {
+        loading.value = true;
+        try {
+            const res = await listTaskRules();
+            rules.value = res.data || [];
+        } catch (e) {
+            rules.value = [];
+            ElMessage.error(e.message);
+        } finally {
+            loading.value = false;
         }
-    ]);
+    };
 
     const handleDelete = (rule) => {
         ElMessageBox.confirm(
@@ -111,123 +104,170 @@
                 confirmButtonClass: 'el-button--danger'
             }
         )
-            .then(() => {
-                // TODO: Call API to delete rule
-                rules.value = rules.value.filter((item) => item.id !== rule.id);
-                ElMessage({
-                    type: 'success',
-                    message: '删除成功'
-                });
+            .then(async () => {
+                try {
+                    const res = await deleteTaskRule(rule.id);
+                    ElMessage.success(actionMessage(res, '删除成功'));
+                    await loadRules();
+                } catch (e) {
+                    ElMessage.error(e.message);
+                }
             })
-            .catch(() => {
-                // Cancelled
-            });
+            .catch(() => {});
     };
+
+    const handleGenerate = async (rule) => {
+        generatingId.value = rule.id;
+        try {
+            const res = await generateTask(rule.id);
+            ElMessage.success(
+                actionMessage(
+                    res,
+                    rule.hasActiveTask ? '更新任务成功' : '生成任务成功'
+                )
+            );
+            await loadRules();
+        } catch (e) {
+            ElMessage.error(e.message);
+        } finally {
+            generatingId.value = null;
+        }
+    };
+
+    onMounted(loadRules);
+
 </script>
 
 <style lang="scss" scoped>
+    .task-settings-page {
+        background: #fff;
+        border-radius: 4px;
+        min-height: calc(100vh - 120px);
+    }
+
+    :deep(.ele-page-content),
+    :deep(.ele-page) {
+        background: #fff;
+    }
+
     .task-settings-container {
-        padding: 20px;
+        padding: 16px 20px 24px;
+        background: #fff;
+    }
+
+    .rule-col {
+        margin-bottom: 16px;
+        display: flex;
     }
 
     .rule-card {
-        background-color: #f5f5f5;
-        border-radius: 4px;
-        padding: 20px;
-        position: relative;
-        height: 100%;
-        min-height: 350px;
+        width: 100%;
         display: flex;
         flex-direction: column;
+        background: #f7f8fa;
+        border: 1px solid #ebeef5;
+        border-radius: 8px;
+        padding: 16px;
+        transition: box-shadow 0.2s ease, border-color 0.2s ease;
+
+        &:hover {
+            border-color: #dcdfe6;
+            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
+        }
+    }
 
-        .card-header {
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-            margin-bottom: 20px;
+    .card-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 12px;
+        padding-bottom: 10px;
+        border-bottom: 1px solid #ebeef5;
 
-            .rule-title {
-                font-size: 16px;
-                font-weight: bold;
-                color: #333;
-            }
+        .rule-title {
+            font-size: 15px;
+            font-weight: 600;
+            color: var(--el-text-color-primary);
+            line-height: 1.4;
+        }
 
-            .delete-icon {
-                cursor: pointer;
-                color: #909399;
-                font-size: 18px;
+        .delete-icon {
+            cursor: pointer;
+            color: var(--el-color-danger);
+            font-size: 24px;
+            padding: 4px;
+            border-radius: 4px;
 
-                &:hover {
-                    color: #f56c6c;
-                }
+            &:hover {
+                color: var(--el-color-danger-dark-2);
+                background: rgba(245, 108, 108, 0.12);
             }
         }
+    }
 
-        .card-content {
-            flex: 1;
-            display: flex;
-            flex-direction: column;
+    .card-content {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        gap: 12px;
+    }
 
-            .rule-desc-section {
-                margin-bottom: 20px;
+    .rule-desc-section {
+        margin-bottom: 0;
 
-                .label {
-                    font-weight: bold;
-                    margin-bottom: 8px;
-                    color: #333;
-                }
+        .label {
+            font-size: 13px;
+            font-weight: 600;
+            color: var(--el-text-color-primary);
+            margin-bottom: 6px;
+        }
 
-                .desc-list {
-                    .desc-item {
-                        color: #606266;
-                        line-height: 1.8;
-                        font-size: 14px;
-                    }
-                }
-            }
+        .desc-list .desc-item {
+            color: var(--el-text-color-regular);
+            line-height: 1.7;
+            font-size: 13px;
+        }
+    }
 
-            .stats-text {
-                color: #67c23a;
-                margin-bottom: 15px;
-                font-size: 14px;
-            }
+    .stats-text {
+        display: inline-flex;
+        align-self: flex-start;
+        max-width: 100%;
+        margin-bottom: 0 !important;
+        padding: 4px 8px;
+        border-radius: 4px;
+        background: rgba(103, 194, 58, 0.1);
+        color: var(--el-color-success);
+        font-size: 13px;
+        line-height: 1.5;
+    }
 
-            .time-info {
-                color: #333;
-                font-size: 14px;
-                line-height: 1.6;
-                margin-bottom: 30px;
-            }
+    .time-info {
+        color: var(--el-text-color-secondary);
+        font-size: 12px;
+        line-height: 1.7;
+        margin-bottom: 0 !important;
+    }
 
-            .action-area {
-                margin-top: auto;
-                text-align: center;
+    .action-area {
+        margin-top: auto;
+        padding-top: 12px;
+        border-top: 1px solid #ebeef5;
+        text-align: center;
 
-                .action-btn {
-                    width: 140px;
-                    background-color: #e1f3d8;
-                    border-color: #e1f3d8;
-                    color: #67c23a;
+        .action-btn {
+            min-width: 132px;
+            height: 32px;
+            padding: 0 16px;
+            background-color: #e1f3d8;
+            border-color: #e1f3d8;
+            color: #67c23a;
 
-                    &:hover {
-                        background-color: #c2e7b0;
-                        border-color: #c2e7b0;
-                    }
-                }
+            &:hover {
+                background-color: #c2e7b0;
+                border-color: #c2e7b0;
             }
         }
     }
 </style>
 
-<style lang="scss">
-    .task-tooltip {
-        max-width: 300px;
-
-        .tooltip-content {
-            p {
-                margin: 5px 0;
-                line-height: 1.5;
-            }
-        }
-    }
-</style>