2026032301

This commit is contained in:
biybuy 2026-03-23 18:03:05 +08:00
parent dc801a0ba1
commit de22605535
31 changed files with 1037 additions and 547 deletions

View File

@ -262,6 +262,7 @@
"useSwipe": true,
"useSystemConfig": true,
"useTabBarStore": true,
"useTableContainerHeight": true,
"useTemplateRef": true,
"useTemplateRefsList": true,
"useTextDirection": true,

View File

@ -43,74 +43,6 @@ OMS(Order Management System,订单管理系统)作为独立服务运行和
4. **出库扫描**:订单发货时完成**出库扫描**,进入待发货/运输中。
5. **闭环完成**:出库扫描完成后,订单进入运输、派送环节,正常订单流程闭环结束。
----------------------------------------------------------------------------------------
## 一、通用设计规范
### 1.1 接口设计规范
#### HTTP 方法约束(仅 POST / GET)
| 方法 | 用途 | 说明 |
| --- | --- | --- |
| **GET** | 查询、详情、列表、导出 | 幂等、无副作用,参数放 Query String |
| **POST** | 新增、修改、删除、状态变更、提交 | 所有变更类操作统一用 POST,参数放 Request Body |
> 因网关/代理/防火墙限制,仅支持 GET/POST 时,更新、删除等操作均通过 POST + 业务动作区分。
#### URL 设计规范
| 规范 | 说明 | 示例 |
| --- | --- | --- |
| 路径小写 | 使用小写字母,短横线分隔 | `/api/order/list`、`/api/sales-product/detail` |
| 资源名词 | URL 表示资源,不包含动词 | `/api/customer` 而非 `/api/getCustomer` |
| 层级清晰 | 最多 3 级,避免过深 | `/api/channel/sales-product/price/list` |
| 动作后缀 | POST 动作可用动词后缀 | `/api/order/delete`、`/api/recharge/approve` |
#### 统一响应格式
```json
{
"code": 0, // 0-成功,非0-业务错误码
"msg": "success", // 提示信息
"data": { } // 业务数据,可为 null
}
```
#### 分页规范
**请求**:`page`(页码,从 1 开始)、`pageSize`(每页条数,默认 20,最大 100)
**响应**:
```json
{
"code": 0, // 0-成功,非0-业务错误码
"msg": "success", // 提示信息
"data": {
"items": [], // 数据列表
"total": 100 // 总条数
}
}
```
#### 错误码规范
| 错误码 | 说明 |
| --- | --- |
| 0 | 成功 |
| 401 | 未登录 |
| 403 | 无权限 |
| 400 | 参数错误 |
| 500 | 服务器内部错误 |
| 1xxxx | 业务错误码(按模块划分) |
#### 其他规范
- **Content-Type**:`application/json`;文件上传用 `multipart/form-data`
- **时间格式**:`yyyy-MM-dd HH:mm:ss` 或 ISO 8601
- **认证**:`Authorization: Bearer {token}` 或 Cookie
- **幂等**:涉及金额、库存等变更的接口需支持幂等(如传 `requestId`)
----------------------------------------------------------------------------------------

View File

@ -81,6 +81,57 @@ a-layout (垂直)
@screen-xxl: 1600px; // 超大屏
```
### 2.5 页面壳、白卡片与列表区尺寸(列表 / Handsontable 类页面)
以下约定与 `default-layout` + `page-layout`、`*-container` / `*-page` 等实现一致,避免「灰底大块留白、列表高度塌缩、内外边距叠成 32px/40px」等问题。
#### 2.5.1 页面层(灰底壳)
| 项目 | 约定 |
|------|------|
| 背景色 | `#f0f3f4`(与布局主内容区一致) |
| 页面根容器 padding | **`16px 16px 24px`**:上/左/右 **16px**,**底 24px** —— 与视口底边留白,列表区随 flex 撑满后,整块内容与屏幕底边保持 **24px** 间距 |
| 参与主内容区高度 | `flex: 1; min-height: 0; display: flex; flex-direction: column;`,与 `PageLayout` 子节点 flex 链衔接 |
| 纵向滚动 | 页面根容器宜 `overflow-y: hidden`(或仅最外层允许滚动),**表格/Handsontable 在内部 `.list-wrap` / `.wtHolder` 滚动**,避免整页与表格双滚动条 |
#### 2.5.2 白卡片(白色内容块)
| 项目 | 约定 |
|------|------|
| 视觉 | 背景 `#fff`,`border-radius: 8px`,`box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08)`(可按页面微调透明度) |
| 撑满剩余高度 | `flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden` |
| 内边距(与列表区配合) | **二选一,避免与 `.list-wrap` 重复叠加**:<br>• **推荐**:白卡片 **`padding: 0`**,由 **页头/搜索行、`.table-toolbar`、`.list-wrap`** 分区控制留白(见下);<br>• **简化页**:白卡片统一 `padding: 16px`,则内部列表区不再重复加左右底 padding |
#### 2.5.3 表格工具栏(`.table-toolbar`)
| 项目 | 约定 |
|------|------|
| 与列表横向对齐 | `padding: 16px 16px 0`(上 16、左右 16,底由与列表的间距承担) |
| 与分页/标题间距 | `margin-bottom: 16px`(分页组件(`PaginationPro`)与列表区 `.list-wrap` 的固定间距) |
| 收缩 | `flex-shrink: 0` |
分页组件放置约束:`PaginationPro` 放在 `.toolbar-right` 内,并与 `toolbar-left` 保持在同一行(同一个 `flex` 行中)展示;工具栏外层使用 `padding: 16px 16px 0` 以保证分页组件距上边/右边均为 16px,且不需要在 `PaginationPro` 外再额外套一层 padding。
有**独立搜索行**(如轨迹配置页)时:搜索框可用 `margin: 16px 16px 16px`,与工具栏、列表区 **左右 16px** 对齐。
#### 2.5.4 列表区(`.list-wrap`)
用于包裹 **HandsontablePro** 或需 **固定测量高度** 的表格容器(配合 `useTableContainerHeight` 的 `tableContainerRef` **挂在本层**)。
| 项目 | 约定 |
|------|------|
| 与内容区四边距离 | **默认**:`padding: 16px`(上、右、下、左均为 **16px**,相对**白卡片内容边缘**或分区后的可用区域,勿与白卡片 padding 重复叠加)。<br>**与 `.list-toolbar` 联用**:当 `.list-toolbar` 使用 `margin-bottom: 16px` 作为“分页组件到列表”的固定间距时,为避免重复留白,建议 `.list-wrap` 使用 `padding: 0 16px 16px`(上 0、左右/下 16)。 |
| 布局 | `flex: 1; min-height: 0; width: 100%; box-sizing: border-box; overflow: hidden; display: flex; flex-direction: column` |
| 触摸/滚动冒泡 | 可选:`overscroll-behavior: contain; touch-action: pan-x pan-y` |
| 子级 Handsontable | 建议 `:deep(.handsontable-pro-container) { flex: 1; min-height: 0; display: flex; flex-direction: column; }`,保证纵向撑满 |
分页组件同一行约束(列表工具栏场景):当页面存在 `.list-toolbar` / `table-toolbar` 时,`PaginationPro` 必须放在右侧同一行的工具栏区域(如 `.toolbar-right`),不要将分页组件单独再做成一行(避免“工具栏一行 + 分页下一行”的不一致布局)。
#### 2.5.5 与布局组件的关系
- `PageLayout` 下直接子节点(路由页根)需能被主内容区赋予高度:项目中通过 `flex: 1; min-height: 0` 等与 `a-layout-content` 链式配合。
- `default-layout` 中对 `[class$='-container']` 的样式仅作用于布局模板内节点;**业务页根类名**(如 `xxx-page`、`track-order-container`)需在**页面自身样式**中按上表补齐 flex 与 padding。
---
## 三、色彩规范
@ -426,8 +477,11 @@ const { t } = useI18n();
### 13.2 容器类
- 页面根容器:`xxx-container`(如 `role-page`、业务相关命名),保证 `padding: 16px`
- 表格工具栏:`table-toolbar`、`toolbar-left`、`toolbar-right`
- 页面根容器:`xxx-container` / `xxx-page`(如 `role-page`、`track-order-container`),**列表类页面**推荐 `padding: 16px 16px 24px`,详见 **§2.5 页面壳、白卡片与列表区尺寸**
- 白卡片:`*-page-card`、`*-section` 等,与 `.list-wrap` 的 padding **勿重复叠加**
- 列表区:`.list-wrap`,**四边 16px**(相对分区后的内容边缘)
- 表格工具栏:`table-toolbar`、`toolbar-left`、`toolbar-right`,工具栏 **`padding: 16px 16px 0`** 与列表对齐
- Handsontable/列表分页工具栏:`.list-toolbar`(当页面没有标准 `table-toolbar` 时),与 `table-toolbar` 保持一致,使用 **`padding: 16px 16px 0`** 且 `margin-bottom: 16px`;`PaginationPro` 必须与 `toolbar-left` 同一行展示(放在 `.toolbar-right` 内,禁止分页另起行),同时 `.list-wrap` 在联用场景建议 `padding-top: 0`(例如 `padding: 0 16px 16px`),实现分页距上/右 16px、距列表 16px
### 13.3 按钮间距
@ -438,8 +492,10 @@ const { t } = useI18n();
## 十四、检查清单(新页面开发)
- [ ] 使用 `xxx-container` 或等效布局结构
- [ ] 工具栏使用 `table-toolbar`、`toolbar-left`、`toolbar-right`
- [ ] 使用 `xxx-container` / `xxx-page` 或等效布局结构;列表页见 **§2.5**:灰底 **`padding: 16px 16px 24px`**、`flex: 1; min-height: 0`
- [ ] 白卡片与 **`.list-wrap`** 内边距勿重复叠加(推荐白卡片 `padding: 0` + 分区 padding)
- [ ] **`.list-wrap`**:`padding: 16px`、`flex: 1; min-height: 0`;需测高时 `ref` 挂在本层
- [ ] 工具栏使用 `table-toolbar`、`toolbar-left`、`toolbar-right`,工具栏 **`padding: 16px 16px 0`**
- [ ] 按钮、输入框符合全局样式(高度、圆角)
- [ ] 使用主题色变量,不硬编码 `#2D8C8C`
- [ ] 弹窗 / 抽屉使用 ModalPro / DrawerPro + useModal
@ -517,4 +573,4 @@ const { t } = useI18n();
---
*文档版本:1.0 | 更新日期:2025-03-21*
*文档版本:1.1 | 更新日期:2026-03-23*

View File

@ -251,6 +251,7 @@ declare global {
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useSystemConfig: typeof import('./composables/useSystemConfig')['useSystemConfig']
const useTabBarStore: typeof import('./store/index')['useTabBarStore']
const useTableContainerHeight: typeof import('./composables/useTableContainerHeight')['useTableContainerHeight']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
@ -561,6 +562,7 @@ declare module 'vue' {
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useSystemConfig: UnwrapRef<typeof import('./composables/useSystemConfig')['useSystemConfig']>
readonly useTabBarStore: UnwrapRef<typeof import('./store/index')['useTabBarStore']>
readonly useTableContainerHeight: UnwrapRef<typeof import('./composables/useTableContainerHeight')['useTableContainerHeight']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>

View File

@ -58,6 +58,10 @@
showCheckboxColumn?: boolean;
// 复选框列表头文案(空则表头为空)
checkboxColumnHeader?: string;
// 序号列表头文案
serialNoHeader?: string;
// 操作列表头文案
actionColumnHeader?: string;
// 复选框变化回调
onCheckboxChange?: (row: any, checked: boolean, rowIndex: number) => void;
// 操作列渲染器
@ -105,6 +109,8 @@
actionColumnWidth: 60,
showCheckboxColumn: false,
checkboxColumnHeader: '',
serialNoHeader: '序号',
actionColumnHeader: '操作',
readOnly: true,
multiValueFields: () => [],
height: undefined,
@ -176,16 +182,23 @@
// 计算表格高度
const calculatedHeight = computed(() => {
if (props.height) {
if (props.height != null && props.height > 0) {
return props.height;
}
const headerHeight = 40;
const rowHeight = 50;
const maxRows = 20;
const minHeight = 1000;
const minHeight = 400;
return Math.max(headerHeight + maxRows * rowHeight, minHeight);
});
/** 外部传入 height 为 0 时不能用 ?? 回退(0 非 nullish),需显式判断 */
const resolveTableHeight = (containerH: number): number => {
if (props.height != null && props.height > 0) return props.height;
if (containerH > 0) return containerH;
return calculatedHeight.value;
};
// 创建省略号渲染器
const createEllipsisRenderer = () => {
return function(instance: any, td: any, row: number, col: number, prop: any, value: any) {
@ -241,7 +254,7 @@
// 操作列(在前面)
if (props.showActionColumn && props.actionColumnPosition === 'start') {
columns.push({ data: currentDataIndex, type: 'text', readOnly: true, width: props.actionColumnWidth, className: 'htCenter', headerClassName: 'htLeft' });
headerTexts.push('操作');
headerTexts.push(props.actionColumnHeader);
currentDataIndex++;
}
@ -255,7 +268,7 @@
headerClassName: 'htLeft',
renderer: ellipsisRenderer
});
headerTexts.push('序号');
headerTexts.push(props.serialNoHeader);
currentDataIndex++;
// 数据列(beforeLast 时最后一列在操作列之后)
@ -264,7 +277,7 @@
// beforeLast:在倒数第二列后插入操作列
if (props.showActionColumn && props.actionColumnPosition === 'beforeLast' && index === dataColCount - 1) {
columns.push({ data: currentDataIndex, type: 'text', readOnly: true, width: props.actionColumnWidth, className: 'htCenter', headerClassName: 'htLeft' });
headerTexts.push('操作');
headerTexts.push(props.actionColumnHeader);
currentDataIndex++;
}
columns.push({
@ -282,7 +295,7 @@
// 操作列(在后面,非 beforeLast 时)
if (props.showActionColumn && props.actionColumnPosition === 'end') {
columns.push({ data: currentDataIndex, type: 'text', readOnly: true, width: props.actionColumnWidth, className: 'htCenter', headerClassName: 'htLeft' });
headerTexts.push('操作');
headerTexts.push(props.actionColumnHeader);
currentDataIndex++;
}
@ -343,7 +356,7 @@
try {
// 未传 height 时使用容器实际高度,使列表高度与容器一致
const containerH = containerRef.value?.clientHeight ?? 0;
const tableHeight = props.height ?? (containerH > 0 ? containerH : calculatedHeight.value);
const tableHeight = resolveTableHeight(containerH);
const hotConfig: any = {
data: data,
@ -511,7 +524,7 @@
// 操作列、序号列:用极简表头替换,去除 div.relative/span.colHeader 等多余容器,确保 60px 下完整显示
if (col === actionColStartIndex || col === serialColIndex || col === actionColEndIndex) {
const label = col === serialColIndex ? '序号' : '操作';
const label = col === serialColIndex ? props.serialNoHeader : props.actionColumnHeader;
TH.innerHTML = '';
TH.classList.add('ht-header-simple');
const span = document.createElement('span');
@ -1069,17 +1082,12 @@
}, 50);
};
// 初始化完成后再次刷新尺寸
// 初始化完成后刷新尺寸(避免与 ResizeObserver 重复狂刷导致 stretchH 列宽错乱)
const finalizeInit = () => {
setTimeout(() => {
if (hotInstance) {
hotInstance.refreshDimensions();
setTimeout(() => {
if (hotInstance) {
hotInstance.refreshDimensions();
}
}, 100);
setTimeout(() => hotInstance?.refreshDimensions(), 100);
}
}, 300);
};
@ -1193,13 +1201,15 @@
if (!containerRef.value) return;
resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && hotInstance && entry.contentRect.height > 0) {
const h = Math.floor(entry.contentRect.height);
hotInstance.updateSettings({ height: h });
nextTick(() => {
hotInstance?.refreshDimensions();
});
}
if (!entry || !hotInstance) return;
const h = entry.contentRect.height;
// 高度以本组件根容器实测为准;勿用外层 props.height 覆盖,否则与 flex 内真实高度不一致,stretchH 会把列压满导致横向条消失
if (h <= 0) return;
const fh = Math.floor(h);
hotInstance.updateSettings({ height: fh });
nextTick(() => {
hotInstance?.refreshDimensions();
});
});
resizeObserver.observe(containerRef.value);
};

View File

@ -80,7 +80,7 @@
:model-value="props.pageSize"
size="small"
class="page-size-select"
:style="{ width: '110px' }"
:style="{ width: '85px' }"
@update:model-value="handlePageSizeChange"
>
<a-option
@ -97,7 +97,7 @@
v-model="jumpPage"
size="small"
class="jump-input"
:style="{ width: '60px' }"
:style="{ width: '50px' }"
@keyup.enter="handleJumpPage"
/>
<span class="page-text">{{ t('common.pagination.page') }}</span>

View File

@ -52,6 +52,9 @@
padding: 16px;
width: 100%;
height: 100%;
box-sizing: border-box;
min-height: 0;
max-width: 100%;
}
.layout {

View File

@ -35,7 +35,7 @@
flex-direction: column;
}
.page-layout-root > * {
.page-layout-root :deep(> *) {
flex: 1;
min-height: 0;
}

View File

@ -109,6 +109,16 @@ export default {
'menu.api.apiDebugger': 'API Debugger',
'menu.api.apiLike': 'API LIKE',
'menu.api.apiTest': 'API Test',
// API - Call Statistics
'api.callStatistics.name.app': 'Application Name',
'api.callStatistics.name.provider': 'Service Provider Name',
'api.callStatistics.table.date': 'Date',
'api.callStatistics.table.interfaceName': 'Interface Name',
'api.callStatistics.table.total': 'Total Calls',
'api.callStatistics.table.succeedCount': 'Successful Calls',
'api.callStatistics.table.failCount': 'Failed Calls',
'api.callStatistics.table.successRate': 'Success Rate',
'menu.system': 'System',
'menu.system.org': 'Organization',
'menu.system.organization': 'Organization',

View File

@ -109,6 +109,16 @@ export default {
'menu.api.apiDebugger': 'API调试',
'menu.api.apiLike': 'API LIKE',
'menu.api.apiTest': 'API测试',
// API - 调用统计
'api.callStatistics.name.app': '应用名称',
'api.callStatistics.name.provider': '服务商名称',
'api.callStatistics.table.date': '日期',
'api.callStatistics.table.interfaceName': '接口名称',
'api.callStatistics.table.total': '总调用次数',
'api.callStatistics.table.succeedCount': '成功调用次数',
'api.callStatistics.table.failCount': '失败调用次数',
'api.callStatistics.table.successRate': '成功率',
'menu.system': '系统',
'menu.system.org': '组织管理',
'menu.system.organization': '组织管理',

View File

@ -6,14 +6,16 @@
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
import { useI18n } from 'vue-i18n';
defineOptions({ name: 'CallStatistics' });
const { t } = useI18n();
const activeName = ref('1'); // 1:应用,2:服务商
const { tableContainerRef, tableHeight } = useTableContainerHeight();
const tabList = [
{ key: '1', label: '应用' },
{ key: '2', label: '服务商' },
{ key: '1', label: t('system.log.options.app') },
{ key: '2', label: t('system.log.options.provider') },
];
const tableData = ref<any[]>([]);
@ -30,16 +32,16 @@
});
const typeParam = computed(() => (activeName.value === '2' ? 2 : 1));
const nameLabel = computed(() => (activeName.value === '2' ? '服务商名称' : '应用名称'));
const nameLabel = computed(() => (activeName.value === '2' ? t('api.callStatistics.name.provider') : t('api.callStatistics.name.app')));
const columns = computed(() => [
{ key: 'date', label: '日期', width: 120, searchable: false },
{ key: 'date', label: t('api.callStatistics.table.date'), width: 120, searchable: false },
{ key: 'name', label: nameLabel.value, width: 180, searchable: true, searchField: 'name' },
{ key: 'interfaceName', label: '接口名称', width: 180, searchable: true, searchField: 'interfaceName' },
{ key: 'total', label: '总调用次数', width: 120, searchable: false },
{ key: 'succeedCount', label: '成功调用次数', width: 140, searchable: false },
{ key: 'failCount', label: '失败调用次数', width: 140, searchable: false },
{ key: 'successRate', label: '成功率', width: 100, searchable: false },
{ key: 'interfaceName', label: t('api.callStatistics.table.interfaceName'), width: 180, searchable: true, searchField: 'interfaceName' },
{ key: 'total', label: t('api.callStatistics.table.total'), width: 120, searchable: false },
{ key: 'succeedCount', label: t('api.callStatistics.table.succeedCount'), width: 140, searchable: false },
{ key: 'failCount', label: t('api.callStatistics.table.failCount'), width: 140, searchable: false },
{ key: 'successRate', label: t('api.callStatistics.table.successRate'), width: 100, searchable: false },
]);
const processedTableData = computed(() =>
@ -121,105 +123,122 @@
</script>
<template>
<div class="call-statistics-container">
<!-- 标签页导航 -->
<div class="tabs-section">
<div class="tabs-container">
<div
v-for="tab in tabList"
:key="tab.key"
:class="['tab-item', { active: activeName === tab.key }]"
@click="handleTabChange(tab.key)"
>
<span class="tab-label">{{ tab.label }}</span>
</div>
</div>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 筛选栏 + 分页 -->
<div class="table-toolbar">
<div class="toolbar-left">
<div class="filter-bar">
<a-range-picker
v-model="headerDateRange"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:style="{ width: '240px' }"
class="filter-date-picker"
/>
<a-input
v-model="searchParams.name"
:placeholder="nameLabel"
class="filter-input"
style="width: 160px"
allow-clear
@keyup.enter="handleHeaderSearch"
@blur="handleHeaderSearch"
/>
<a-input
v-model="searchParams.interfaceName"
placeholder="接口名称"
class="filter-input"
style="width: 160px"
allow-clear
@keyup.enter="handleHeaderSearch"
@blur="handleHeaderSearch"
/>
</div>
</div>
<div class="toolbar-right">
<div class="pagination-controls">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
<div class="callStatistics-container">
<div class="callStatistics-page-card">
<!-- 标签页导航 -->
<div class="tabs-section">
<div class="tabs-container">
<div
v-for="tab in tabList"
:key="tab.key"
:class="['tab-item', { active: activeName === tab.key }]"
@click="handleTabChange(tab.key)"
>
<span class="tab-label">{{ tab.label }}</span>
</div>
</div>
</div>
<!-- Handsontable 表格 -->
<div ref="tableContainerRef" class="table-wrapper">
<HandsontablePro
:key="`table-${activeName}-${currentPage}`"
:columns="columns"
:data="processedTableData"
:total="Number(totalCount) || 0"
:current-page="currentPage"
:page-size="pageSize"
:search-params="searchParams"
:height="tableHeight"
:stretch-columns="true"
:show-action-column="false"
:show-checkbox-column="false"
@search="handleHeaderSearch"
/>
<!-- 表格区域 -->
<div class="table-section">
<!-- 筛选栏 + 分页 -->
<div class="table-toolbar">
<div class="toolbar-left">
<div class="filter-bar">
<a-range-picker
v-model="headerDateRange"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:style="{ width: '240px' }"
class="filter-date-picker"
/>
<a-input
v-model="searchParams.name"
:placeholder="nameLabel"
class="filter-input"
style="width: 160px"
allow-clear
@keyup.enter="handleHeaderSearch"
@blur="handleHeaderSearch"
/>
<a-input
v-model="searchParams.interfaceName"
:placeholder="t('system.log.placeholder.interfaceName')"
class="filter-input"
style="width: 160px"
allow-clear
@keyup.enter="handleHeaderSearch"
@blur="handleHeaderSearch"
/>
</div>
</div>
<div class="toolbar-right">
<div class="pagination-controls">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div>
</div>
<!-- Handsontable 表格 -->
<div ref="tableContainerRef" class="list-wrap">
<HandsontablePro
:key="`table-${activeName}-${currentPage}`"
:columns="columns"
:data="processedTableData"
:total="Number(totalCount) || 0"
:current-page="currentPage"
:page-size="pageSize"
:search-params="searchParams"
:height="tableHeight"
:stretch-columns="true"
:show-action-column="false"
:show-checkbox-column="false"
@search="handleHeaderSearch"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.call-statistics-container {
padding: 16px;
.callStatistics-container {
padding: 16px 16px 24px;
background-color: #f0f3f4;
height: 100vh;
flex: 1;
min-height: 0;
overflow: hidden;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.callStatistics-page-card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.tabs-section {
background: #fff;
border-radius: 8px;
padding: 0 24px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px 16px 0;
margin-bottom: 8px;
flex-shrink: 0;
background: transparent;
box-shadow: none;
border-radius: 0;
}
.tabs-container {
@ -255,18 +274,20 @@
}
.table-section {
background: #fff;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
height: calc(100vh - 80px);
overflow: visible;
background: transparent;
border-radius: 0;
padding: 0;
box-shadow: none;
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
width: 100%;
}
.table-toolbar {
padding: 16px 16px 0;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
@ -305,8 +326,26 @@
.table-wrapper {
flex: 1;
min-height: 0;
overflow: auto;
overflow: hidden;
width: 100%;
height: 100%;
}
.list-wrap {
flex: 1;
min-height: 0;
width: 100%;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 16px;
:deep(.handsontable-pro-container) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
}
</style>

View File

@ -681,10 +681,11 @@
}
.table-toolbar {
margin-bottom: 8px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
.toolbar-left {
flex: 1;

View File

@ -1,7 +1,13 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { computed, nextTick, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { $baseMessage } from '@/utils'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import FormPro from '@/components/FormPro/index.vue'
import dayjs from 'dayjs'
defineOptions({
name: 'AutoTrigger'
@ -9,8 +15,20 @@ defineOptions({
const { t } = useI18n()
// 表格数据
const tableData = ref([
type AutoTriggerRow = {
id: number
status: boolean
operation: string
triggerTiming: string
triggerCondition: string
promptMessage: string
notifier: string
createTime: string
creator: string
}
// 表格数据(模拟)
const tableData = ref<AutoTriggerRow[]>([
{
id: 1,
status: true,
@ -49,27 +67,221 @@ const tableData = ref([
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const totalCount = ref(tableData.value.length)
const totalCount = computed(() => tableData.value.length)
// 当前页数据(HandsontablePro 内部不做分页,只负责渲染当前 data)
const processedTableData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return tableData.value.slice(start, start + pageSize.value)
})
// 切换状态
const handleToggleStatus = (row: any) => {
function handleToggleStatus(row: AutoTriggerRow) {
row.status = !row.status
$baseMessage(row.status ? t('channel.autoTrigger.message.statusEnabled') : t('channel.autoTrigger.message.statusDisabled'), 'success')
$baseMessage(
row.status ? t('channel.autoTrigger.message.statusEnabled') : t('channel.autoTrigger.message.statusDisabled'),
'success'
)
}
// 删除
const handleDelete = (row: any) => {
function handleDelete(row: AutoTriggerRow) {
$baseMessage(t('channel.autoTrigger.message.deletePending'), 'info')
}
// 编辑
const handleEdit = (row: any) => {
$baseMessage(t('channel.autoTrigger.message.editPending'), 'info')
type ModalMode = 'add' | 'edit'
const modalMode = ref<ModalMode>('add')
const modalTitle = computed(() =>
modalMode.value === 'add' ? t('channel.autoTrigger.actions.add') : t('channel.autoTrigger.actions.edit')
)
const [registerAutoTriggerModal, modalMethods] = useModal() as any
const formRef = ref<InstanceType<typeof FormPro> | null>(null)
const editId = ref<number | null>(null)
const formState = reactive({
status: true,
operation: '拦截',
triggerTiming: '',
triggerCondition: '',
promptMessage: '',
notifier: '',
createTime: '',
creator: '',
})
const formItemList = computed(() => {
const requiredMsg = t('system.serviceProviders.modal.rules.requiredInput')
return [
{
label: t('channel.autoTrigger.table.status'),
field: 'status',
comp: 'ASelect',
span: 24,
rules: [{ required: true, message: requiredMsg }],
compProps: {
placeholder: t('common.selectPlaceholder'),
options: [
{ label: t('channel.autoTrigger.message.statusEnabled'), value: true },
{ label: t('channel.autoTrigger.message.statusDisabled'), value: false },
],
},
},
{
label: t('channel.autoTrigger.table.operation'),
field: 'operation',
comp: 'ASelect',
span: 24,
rules: [{ required: true, message: requiredMsg }],
compProps: {
placeholder: t('common.selectPlaceholder'),
options: [
{ label: '拦截', value: '拦截' },
{ label: '警告', value: '警告' },
{ label: '问题件', value: '问题件' },
],
},
},
{
label: t('channel.autoTrigger.table.triggerTiming'),
field: 'triggerTiming',
comp: 'AInput',
span: 24,
rules: [{ required: true, message: requiredMsg }],
compProps: { placeholder: t('channel.area.form.placeholder.input') },
},
{
label: t('channel.autoTrigger.table.triggerCondition'),
field: 'triggerCondition',
comp: 'ATextarea',
span: 24,
rules: [{ required: true, message: requiredMsg }],
compProps: {
placeholder: t('channel.area.form.placeholder.input'),
autoSize: { minRows: 3, maxRows: 6 },
showWordLimit: true,
maxlength: 1000,
},
},
{
label: t('channel.autoTrigger.table.promptMessage'),
field: 'promptMessage',
comp: 'AInput',
span: 24,
rules: [{ required: true, message: requiredMsg }],
compProps: { placeholder: t('channel.area.form.placeholder.input') },
},
{
label: t('channel.autoTrigger.table.notifier'),
field: 'notifier',
comp: 'AInput',
span: 24,
compProps: { placeholder: t('channel.area.form.placeholder.input'), allowClear: true },
},
{
label: t('channel.autoTrigger.table.createTime'),
field: 'createTime',
comp: 'AInput',
span: 24,
compProps: { disabled: true },
},
{
label: t('channel.autoTrigger.table.creator'),
field: 'creator',
comp: 'AInput',
span: 24,
compProps: { disabled: true },
},
]
})
async function openAddModal() {
modalMode.value = 'add'
editId.value = null
Object.assign(formState, {
status: true,
operation: '拦截',
triggerTiming: '',
triggerCondition: '',
promptMessage: '',
notifier: '',
createTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
creator: '当前用户',
})
modalMethods.setModalProps({ title: modalTitle.value, width: 720 })
modalMethods.openModal(true)
await nextTick()
formRef.value?.setValues?.(formState)
}
// 新增
const handleAdd = () => {
$baseMessage(t('channel.autoTrigger.message.addPending'), 'info')
async function openEditModal(row: AutoTriggerRow) {
modalMode.value = 'edit'
editId.value = row.id
Object.assign(formState, {
status: row.status,
operation: row.operation,
triggerTiming: row.triggerTiming,
triggerCondition: row.triggerCondition,
promptMessage: row.promptMessage,
notifier: row.notifier,
createTime: row.createTime,
creator: row.creator,
})
modalMethods.setModalProps({ title: modalTitle.value, width: 720 })
modalMethods.openModal(true)
await nextTick()
formRef.value?.setValues?.(formState)
}
// 编辑入口(Handsontable 操作列)
function handleEdit(row: AutoTriggerRow) {
openEditModal(row)
}
// 新增入口(页面新增按钮)
function handleAdd() {
openAddModal()
}
async function handleAutoTriggerModalOk() {
try {
await formRef.value?.validate()
const values = formRef.value?.getValues?.() || {}
if (modalMode.value === 'add') {
const newRow: AutoTriggerRow = {
id: Date.now(),
...values,
status: !!values.status,
notifier: values.notifier || '',
}
tableData.value.unshift(newRow)
currentPage.value = 1
$baseMessage(t('common.addSuccess'), 'success')
} else {
const id = editId.value
if (id == null) return
const idx = tableData.value.findIndex((d) => d.id === id)
if (idx >= 0) {
tableData.value[idx] = {
...tableData.value[idx],
...values,
status: !!values.status,
notifier: values.notifier || '',
}
$baseMessage(t('common.editSuccess'), 'success')
}
}
modalMethods.closeModal()
} catch (_) {
// 校验失败由表单组件自行提示
}
}
function handleAutoTriggerModalCancel() {
modalMethods.closeModal()
}
// 分页改变
@ -82,33 +294,142 @@ const handlePageSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1
}
const hotColumns = computed(() => [
{ key: 'status', label: t('channel.autoTrigger.table.status'), width: 80 },
{ key: 'operation', label: t('channel.autoTrigger.table.operation'), width: 110 },
{ key: 'triggerTiming', label: t('channel.autoTrigger.table.triggerTiming'), width: 110 },
{ key: 'triggerCondition', label: t('channel.autoTrigger.table.triggerCondition'), width: 320 },
{ key: 'promptMessage', label: t('channel.autoTrigger.table.promptMessage'), width: 240 },
{ key: 'notifier', label: t('channel.autoTrigger.table.notifier'), width: 120 },
{ key: 'createTime', label: t('channel.autoTrigger.table.createTime'), width: 170 },
{ key: 'creator', label: t('channel.autoTrigger.table.creator'), width: 110 }
])
// 与其他 Handsontable 页面一致:捕获阶段处理内联开关,避免 Handsontable 抢事件
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 400))
function handleStatusSwitchMouseDown(e: MouseEvent) {
const switchEl = (e.target as HTMLElement | null)?.closest?.('.customer-status-switch') as HTMLElement | null
if (!switchEl) return
e.preventDefault()
e.stopPropagation()
const rowIndex = parseInt(switchEl.getAttribute('data-row-index') || '0', 10)
const idx = (currentPage.value - 1) * pageSize.value + rowIndex
const record = tableData.value[idx]
if (record) {
handleToggleStatus(record)
}
}
function customCellRenderer(row: number, col: number, value: any, rowData: AutoTriggerRow): string | null {
// HandsontablePro 列结构(本页不显示 checkbox,action 列在末尾):
// col=0: 序号;col=1..n: 数据列
const dataColIndex = col - 1
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
const colKey = hotColumns.value[dataColIndex].key
if (colKey === 'status') {
const enabled = !!rowData.status
return `
<label class="customer-status-switch" data-row-index="${row}" style="display:inline-flex;align-items:center;cursor:pointer;">
<input type="checkbox" ${enabled ? 'checked' : ''} style="display:none;" />
<span class="switch-track" style="position:relative;width:36px;height:20px;background:${enabled ? '#00b42a' : '#e5e6eb'};border-radius:10px;transition:background .2s;">
<span class="switch-thumb" style="position:absolute;top:2px;left:${enabled ? '18px' : '2px'};width:16px;height:16px;background:#fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2);transition:left .2s;"></span>
</span>
</label>
`
}
if (colKey === 'operation') {
const op = rowData.operation
const cls = op === '拦截' ? 'intercept' : op === '警告' ? 'warning' : op === '问题件' ? 'problem' : ''
return `<span class="operation-tag ${cls}">${op}</span>`
}
return null
}
function actionRenderer(row: AutoTriggerRow, rowIndex: number): string {
return `
<div class="action-btns" style="display:flex;gap:12px;align-items:center;justify-content:center;">
<span
data-action="delete"
data-row-index="${rowIndex}"
title="${t('channel.autoTrigger.actions.delete')}"
style="cursor:pointer;color:#f53f3f;display:inline-flex;align-items:center;justify-content:center;"
>
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
<path d="M5 11h5.5m0 0v29a1 1 0 0 0 1 1h25a1 1 0 0 0 1-1V11m-27 0H16m21.5 0H43m-5.5 0H32m-16 0V7h16v4m-16 0h16M20 18v15m8-15v15"/>
</svg>
</span>
<span
data-action="edit"
data-row-index="${rowIndex}"
title="${t('channel.autoTrigger.actions.edit')}"
style="cursor:pointer;color:var(--app-primary-color);display:inline-flex;align-items:center;justify-content:center;"
>
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
<path d="m30.48 19.038 5.733-5.734a1 1 0 0 0 0-1.414l-5.586-5.586a1 1 0 0 0-1.414 0l-5.734 5.734m7 7L15.763 33.754a1 1 0 0 1-.59.286l-6.048.708a1 1 0 0 1-1.113-1.069l.477-6.31a1 1 0 0 1 .29-.631l14.7-14.7m7 7-7-7M6 42h36" />
</svg>
</span>
</div>
`
}
function handleActionClick(row: AutoTriggerRow, rowIndex: number, action?: string) {
const idx = (currentPage.value - 1) * pageSize.value + rowIndex
const record = tableData.value[idx] || row
switch (action) {
case 'delete':
handleDelete(record)
break
case 'edit':
default:
handleEdit(record)
break
}
}
const handsontableRef = ref<InstanceType<typeof HandsontablePro> | null>(null)
</script>
<style lang="less" scoped>
.auto-trigger-layout {
display: flex;
height: 100%;
padding: 16px;
gap: 16px;
background-color: var(--color-fill-2);
overflow: hidden;
}
.right-content.content-panel {
flex: 1;
background-color: var(--color-bg-1);
border-radius: 24px;
.autoTrigger-container {
width: 100%;
box-sizing: border-box;
padding: 16px 16px 24px;
display: flex;
flex-direction: column;
background-color: #f0f3f4;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: hidden;
}
.autoTrigger-page-card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 8px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
padding: 20px;
}
.table-toolbar {
padding: 16px 16px 0;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
flex-wrap: nowrap;
.toolbar-left {
flex: 1;
@ -229,6 +550,28 @@ const handlePageSizeChange = (size: number) => {
position: relative;
}
.list-wrap {
width: 100%;
min-width: 0;
flex: 1;
min-height: 0;
overflow: hidden;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overscroll-behavior: contain;
touch-action: pan-x pan-y;
:deep(.handsontable-pro-container) {
flex: 1;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
}
}
.custom-table-header {
background: #f7f8fa;
border-bottom: 1px solid #e5e6eb;
@ -431,8 +774,12 @@ const handlePageSizeChange = (size: number) => {
}
.empty-state {
padding: 40px 0;
text-align: center;
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
// 状态开关样式
@ -444,32 +791,33 @@ const handlePageSizeChange = (size: number) => {
}
// 操作类型样式
.operation-tag {
:deep(.operation-tag) {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.intercept {
background: #ffe7ba;
color: #d46b08;
}
&.warning {
background: #fff2e8;
color: #d4380d;
}
&.problem {
background: #fff1f0;
color: #cf1322;
}
}
// HandsontablePro 单元格使用 innerHTML 注入,需用 :deep 让样式穿透 scoped
:deep(.operation-tag.intercept) {
background: #ffe7ba;
color: #d46b08;
}
:deep(.operation-tag.warning) {
background: #fff2e8;
color: #d4380d;
}
:deep(.operation-tag.problem) {
background: #fff1f0;
color: #cf1322;
}
</style>
<template>
<div class="auto-trigger-layout">
<div class="right-content content-panel">
<div class="autoTrigger-container">
<div class="autoTrigger-page-card">
<!-- 表格操作按钮和分页控制 -->
<div class="table-toolbar">
<div class="toolbar-left">
@ -517,116 +865,54 @@ const handlePageSizeChange = (size: number) => {
</div>
</div>
</div>
<!-- 自定义表格 -->
<div class="custom-table-container">
<!-- 自定义表头 -->
<div class="custom-table-header">
<div class="header-row">
<div class="header-cell index-cell">
{{ t('channel.autoTrigger.table.serialNo') }}
</div>
<div class="header-cell status-cell">
{{ t('channel.autoTrigger.table.status') }}
</div>
<div class="header-cell operation-cell">
{{ t('channel.autoTrigger.table.operation') }}
</div>
<div class="header-cell timing-cell">
{{ t('channel.autoTrigger.table.triggerTiming') }}
</div>
<div class="header-cell condition-cell">
{{ t('channel.autoTrigger.table.triggerCondition') }}
</div>
<div class="header-cell message-cell">
{{ t('channel.autoTrigger.table.promptMessage') }}
</div>
<div class="header-cell notifier-cell">
{{ t('channel.autoTrigger.table.notifier') }}
</div>
<div class="header-cell time-cell">
{{ t('channel.autoTrigger.table.createTime') }}
</div>
<div class="header-cell creator-cell">
{{ t('channel.autoTrigger.table.creator') }}
</div>
<div class="header-cell action-cell">
{{ t('channel.autoTrigger.table.actions') }}
</div>
</div>
</div>
<!-- 表格内容 -->
<div class="custom-table-body">
<div
v-for="(row, index) in tableData"
:key="row.id"
class="table-row"
>
<div class="table-cell index-cell">
{{ index + 1 }}
</div>
<div class="table-cell status-cell">
<a-switch
v-model="row.status"
class="status-switch"
@change="handleToggleStatus(row)"
/>
</div>
<div class="table-cell operation-cell">
<span
class="operation-tag"
:class="{
intercept: row.operation === '拦截',
warning: row.operation === '警告',
problem: row.operation === '问题件'
}"
>
{{ row.operation }}
</span>
</div>
<div class="table-cell timing-cell">
{{ row.triggerTiming }}
</div>
<div class="table-cell condition-cell">
{{ row.triggerCondition }}
</div>
<div class="table-cell message-cell">
{{ row.promptMessage }}
</div>
<div class="table-cell notifier-cell">
{{ row.notifier || '--' }}
</div>
<div class="table-cell time-cell">
{{ row.createTime }}
</div>
<div class="table-cell creator-cell">
{{ row.creator }}
</div>
<div class="table-cell action-cell">
<div class="action-buttons">
<a
class="action-link"
@click="handleDelete(row)"
>
{{ t('channel.autoTrigger.actions.delete') }}
</a>
<a
class="action-link"
@click="handleEdit(row)"
>
{{ t('channel.autoTrigger.actions.edit') }}
</a>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!tableData.length" class="empty-state">
<a-empty :description="t('channel.autoTrigger.empty')" />
</div>
</div>
<!-- HandsontablePro 列表区 -->
<div v-if="!tableData.length" class="empty-state">
<a-empty :description="t('channel.autoTrigger.empty')" />
</div>
<div
v-else
ref="tableContainerRef"
class="list-wrap"
@mousedown.capture="handleStatusSwitchMouseDown"
>
<HandsontablePro
ref="handsontableRef"
:columns="hotColumns"
:data="processedTableData"
:total="totalCount"
:current-page="currentPage"
:page-size="pageSize"
:show-action-column="true"
:action-column-position="'end'"
:action-column-width="100"
:serial-no-header="t('channel.autoTrigger.table.serialNo')"
:action-column-header="t('channel.autoTrigger.table.actions')"
:action-renderer="actionRenderer"
:custom-cell-renderer="customCellRenderer"
:stretch-columns="true"
:height="handsontableHeight"
@action-click="handleActionClick"
/>
</div>
<ModalPro
:title="modalTitle"
:width="720"
@register="registerAutoTriggerModal"
@ok="handleAutoTriggerModalOk"
@cancel="handleAutoTriggerModalCancel"
>
<FormPro
ref="formRef"
:item-list="formItemList"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 16 }"
:row-gutter="20"
:default-span="6"
form-layout="horizontal"
/>
</ModalPro>
</div>
</div>
</template>

View File

@ -1,16 +1,18 @@
<template>
<div class="label-design-page">
<FullDesigner
ref="designerRef"
default-lang="cn"
:hi-auto-connect="false"
@save="onSave"
>
<!-- 覆盖默认左侧区域,隐藏 logo + 标题(插槽可能不生效,配合 CSS 隐藏 .header-left) -->
<template #headerLeft></template>
<!-- 覆盖默认右侧按钮区域,彻底隐藏“反馈”等按钮 -->
<template #headerRight></template>
</FullDesigner>
<div class="labelDesign-container">
<div class="labelDesign-page-card">
<FullDesigner
ref="designerRef"
default-lang="cn"
:hi-auto-connect="false"
@save="onSave"
>
<!-- 覆盖默认左侧区域,隐藏 logo + 标题(插槽可能不生效,配合 CSS 隐藏 .header-left) -->
<template #headerLeft></template>
<!-- 覆盖默认右侧按钮区域,彻底隐藏“反馈”等按钮 -->
<template #headerRight></template>
</FullDesigner>
</div>
</div>
</template>
@ -43,35 +45,57 @@ function onSave(payload: { template: unknown; data: unknown }) {
</script>
<style scoped>
.label-design-page {
padding: 16px;
background-color: var(--color-bg-1);
min-height: calc(100vh - 86px);
.labelDesign-container {
padding: 16px 16px 24px;
background-color: #f0f3f4;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.labelDesign-page-card {
flex: 1;
min-height: 0;
background: #fff;
border-radius: 8px;
overflow: hidden;
}
/* 顶部栏背景透明(第一行 .primary-toolbar + 第二行 .secondary-toolbar) */
.label-design-page :deep(.primary-toolbar),
.label-design-page :deep(.primary-toolbar .toolbar-content),
.label-design-page :deep(.secondary-toolbar),
.label-design-page :deep(.secondary-toolbar .toolbar-content) {
.labelDesign-page-card :deep(.primary-toolbar),
.labelDesign-page-card :deep(.primary-toolbar .toolbar-content),
.labelDesign-page-card :deep(.secondary-toolbar),
.labelDesign-page-card :deep(.secondary-toolbar .toolbar-content) {
background: transparent !important;
background-color: transparent !important;
}
/* 顶部中间/右侧区域也透明 */
.label-design-page :deep(.header-center),
.label-design-page :deep(.header-right.toolbar-right) {
.labelDesign-page-card :deep(.header-center),
.labelDesign-page-card :deep(.header-right.toolbar-right) {
background: transparent !important;
background-color: transparent !important;
}
/* 隐藏 vg-print 顶部左侧 logo + 文本(打包代码中为 .header-left / .toolbar-logo / .toolbar-title) */
.label-design-page :deep(.header-left) {
.labelDesign-page-card :deep(.header-left) {
display: none !important;
}
/* 隐藏 vg-print 内置的反馈/帮助按钮(class: btn-feedback) */
.label-design-page :deep(.btn-feedback) {
.labelDesign-page-card :deep(.btn-feedback) {
display: none !important;
}
/* 让 FullDesigner 在白卡片内纵向撑满 */
.labelDesign-page-card :deep(.vg-print),
.labelDesign-page-card :deep(.full-designer),
.labelDesign-page-card :deep(.fullDesigner),
.labelDesign-page-card :deep(.designer-root) {
height: 100%;
flex: 1;
min-height: 0;
}
</style>

View File

@ -1,16 +1,5 @@
<template>
<div class="waybill-container">
<!-- 页面标题栏 -->
<div class="page-header">
<div class="header-left">
<h2 class="page-title">{{ $t('operation.inbound.pageTitle') }}</h2>
</div>
<div class="header-right">
<icon-notification class="sound-icon" />
<span class="broadcast-text">{{ $t('operation.inbound.broadcastSettings') }}</span>
</div>
</div>
<!-- 表单区域 -->
<div class="form-section">
<a-form :model="formData" layout="vertical" class="waybill-form">
@ -110,7 +99,7 @@
<div class="recent-orders-section">
<div class="list-toolbar">
<div class="toolbar-left">
<span class="section-subtitle">{{ $t('operation.inbound.recentOrders.subtitle') }}</span>
</div>
<div class="toolbar-right">
<div class="pagination-controls">
@ -144,7 +133,6 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { ref, reactive, computed, onMounted } from 'vue';
import { IconNotification } from '@arco-design/web-vue/es/icon';
import { $baseMessage } from '@/utils';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
@ -303,8 +291,8 @@ onMounted(() => {
.waybill-container {
padding: 24px;
background-color: #f5f5f5;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
display: flex;
@ -449,9 +437,10 @@ onMounted(() => {
.recent-orders-section {
background: #fff;
border-radius: 8px;
padding: 24px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex: 0 0 auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
@ -459,8 +448,10 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
flex-wrap: nowrap;
.toolbar-left .section-subtitle {
font-size: 12px;
@ -490,14 +481,17 @@ onMounted(() => {
}
}
// Handsontable 列表区固定高度,父级 flex 链供 HandsontablePro 撑满
.list-wrap {
flex: 0 0 auto;
height: 850px;
width: 100%;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
overscroll-behavior: contain;
touch-action: pan-x pan-y;
}
}
</style>

View File

@ -1,16 +1,5 @@
<template>
<div class="waybill-container">
<!-- 页面标题栏 -->
<div class="page-header">
<div class="header-left">
<h2 class="page-title">{{ $t('operation.outbound.pageTitle') }}</h2>
</div>
<div class="header-right">
<icon-notification class="sound-icon" />
<span class="broadcast-text">{{ $t('operation.outbound.broadcastSettings') }}</span>
</div>
</div>
<!-- 表单区域 -->
<div class="form-section">
<a-form :model="formData" layout="vertical" class="waybill-form">
@ -39,7 +28,7 @@
<div class="recent-orders-section">
<div class="list-toolbar">
<div class="toolbar-left">
<span class="section-subtitle">{{ $t('operation.outbound.recentOrders.subtitle') }}</span>
</div>
<div class="toolbar-right">
<div class="pagination-controls">
@ -73,7 +62,6 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { ref, reactive, computed, onMounted } from 'vue';
import { IconNotification } from '@arco-design/web-vue/es/icon';
import { $baseMessage } from '@/utils';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
@ -189,8 +177,8 @@ onMounted(() => {
.waybill-container {
padding: 24px;
background-color: #f5f5f5;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
display: flex;
@ -273,9 +261,10 @@ onMounted(() => {
.recent-orders-section {
background: #fff;
border-radius: 8px;
padding: 24px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex: 0 0 auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
@ -283,8 +272,10 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
flex-wrap: nowrap;
.toolbar-left .section-subtitle {
font-size: 12px;
@ -315,12 +306,16 @@ onMounted(() => {
}
.list-wrap {
flex: 0 0 auto;
height: 850px;
width: 100%;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
overscroll-behavior: contain;
touch-action: pan-x pan-y;
}
}
</style>

View File

@ -1,16 +1,5 @@
<template>
<div class="waybill-container">
<!-- 页面标题栏 -->
<div class="page-header">
<div class="header-left">
<h2 class="page-title">{{ $t('operation.printLabel.pageTitle') }}</h2>
</div>
<div class="header-right">
<icon-notification class="sound-icon" />
<span class="broadcast-text">{{ $t('operation.printLabel.broadcastSettings') }}</span>
</div>
</div>
<!-- 表单区域 -->
<div class="form-section">
<a-form :model="formData" layout="vertical" class="waybill-form">
@ -86,7 +75,7 @@
<div class="recent-orders-section">
<div class="list-toolbar">
<div class="toolbar-left">
<span class="section-subtitle">{{ $t('operation.printLabel.recentOrders.subtitle') }}</span>
</div>
<div class="toolbar-right">
<div class="pagination-controls">
@ -120,7 +109,6 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { ref, reactive, computed, onMounted } from 'vue';
import { IconNotification } from '@arco-design/web-vue/es/icon';
import { $baseMessage } from '@/utils';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
@ -302,8 +290,8 @@ function handleReset() {
.waybill-container {
padding: 24px;
background-color: #f5f5f5;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
display: flex;
@ -400,9 +388,10 @@ function handleReset() {
.recent-orders-section {
background: #fff;
border-radius: 8px;
padding: 24px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex: 0 0 auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
@ -410,8 +399,10 @@ function handleReset() {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
flex-wrap: nowrap;
.toolbar-left .section-subtitle {
font-size: 12px;
@ -442,12 +433,16 @@ function handleReset() {
}
.list-wrap {
flex: 0 0 auto;
height: 850px;
width: 100%;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
overscroll-behavior: contain;
touch-action: pan-x pan-y;
}
}
</style>

View File

@ -521,12 +521,14 @@
<style lang="less" scoped>
.my-orders-container {
padding: 16px;
width: 100%;
box-sizing: border-box;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
}
@ -781,18 +783,22 @@
.table-section {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex: 0 0 auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-toolbar {
margin-bottom: 8px;
padding: 16px 16px 0;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
.toolbar-left {
flex: 1;
@ -820,11 +826,22 @@
.list-wrap {
width: 100%;
overflow: hidden;
flex: 0 0 auto;
height: 1150px;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overscroll-behavior: contain;
touch-action: pan-x pan-y;
:deep(.handsontable-pro-container) {
width: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.handsontable-wrapper {
width: 100%;

View File

@ -319,10 +319,15 @@
<style lang="less" scoped>
.place-order-container {
padding: 24px;
background-color: #f5f5f5;
height: 100vh;
padding: 16px 16px 24px;
background-color: #f0f3f4;
flex: 1;
min-height: 0;
box-sizing: border-box;
overflow: hidden;
width: 100%;
display: flex;
flex-direction: column;
}
.page-header {
@ -339,7 +344,9 @@
.main-content {
display: flex;
gap: 24px;
margin-bottom: 24px;
flex: 1;
min-height: 0;
margin-bottom: 0;
}
.left-column {
@ -394,7 +401,7 @@
transition: all 0.3s;
&:hover {
border-color: #2D8C8C;
border-color: var(--app-primary-color);
background: #f0f9f9;
}
@ -525,7 +532,7 @@
}
&:hover {
color: #2D8C8C !important;
color: var(--app-primary-color) !important;
}
}
}
@ -556,7 +563,7 @@
transition: all 0.3s;
&:hover {
border-color: #2D8C8C;
border-color: var(--app-primary-color);
background: #f0f9f9;
}

View File

@ -332,12 +332,13 @@
<style lang="less" scoped>
.price-check-container {
padding: 16px;
width: 100%;
flex: 1;
min-height: 0;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
@ -428,9 +429,9 @@
.table-section {
background: #fff;
border-radius: 8px;
padding: 12px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex: 0 0 auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
@ -438,6 +439,7 @@
}
.table-toolbar {
padding: 16px 16px 0;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
@ -447,9 +449,10 @@
.list-wrap {
width: 100%;
flex: 0 0 auto;
height: 900px;
flex: 1;
min-height: 0;
padding: 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
@ -457,6 +460,14 @@
touch-action: pan-x pan-y;
}
.list-wrap :deep(.handsontable-pro-container) {
width: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
:deep(.order-action-link) {
color: var(--app-primary-color);
font-weight: 500;

View File

@ -20,6 +20,7 @@
</a-space>
</div>
<div class="organization-table-wrap">
<a-table
:columns="columns"
:data="dataSource"
@ -81,6 +82,7 @@
</template>
</a-table>
</div>
</div>
<!-- 新增/编辑组织弹窗 -->
<a-modal
@ -350,31 +352,47 @@ async function handleDelete(id: string | undefined) {
.organization-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
}
.organization-page-card {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 16px 0;
margin-bottom: 8px;
flex-shrink: 0;
gap: 12px;
}
.organization-table-wrap {
flex: 1;
min-height: 0;
overflow: auto;
padding: 0 16px 16px;
box-sizing: border-box;
}
.action-buttons {
display: flex;
align-items: center;

View File

@ -522,20 +522,24 @@ async function loadOrganizations() {
.user-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
}
.user-page-card {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
@ -544,16 +548,19 @@ async function loadOrganizations() {
.page-header {
display: flex;
justify-content: space-between;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
gap: 12px;
flex-wrap: nowrap;
}
.list-wrap {
width: 100%;
flex: 0 0 auto;
height: 1200px;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
overflow: hidden;
overscroll-behavior: contain;
touch-action: pan-x pan-y;

View File

@ -20,6 +20,7 @@
</a-space>
</div>
<div class="menu-table-wrap">
<a-table
:columns="columns"
:data="dataSource"
@ -86,6 +87,7 @@
</template>
</a-table>
</div>
</div>
<!-- 新增/编辑菜单弹窗 -->
<a-modal
@ -394,31 +396,47 @@ function getIcon(iconName: string) {
.menu-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
}
.menu-page-card {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
gap: 12px;
}
.menu-table-wrap {
flex: 1;
min-height: 0;
overflow: auto;
padding: 0 16px 16px;
box-sizing: border-box;
}
.action-buttons {
display: flex;
align-items: center;

View File

@ -470,20 +470,24 @@ onMounted(() => {
.role-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
}
.role-page-card {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
@ -492,16 +496,19 @@ onMounted(() => {
.page-header {
display: flex;
justify-content: space-between;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
gap: 12px;
flex-wrap: nowrap;
}
.list-wrap {
width: 100%;
flex: 0 0 auto;
height: 1200px;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
overflow: hidden;
overscroll-behavior: contain;
touch-action: pan-x pan-y;

View File

@ -296,24 +296,32 @@ onMounted(() => {
.warehouse-address-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
}
.warehouse-address-page-card {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-header {
margin-bottom: 16px;
flex-shrink: 0;
padding: 16px 16px 0;
.page-title {
margin: 0;
@ -324,6 +332,11 @@ onMounted(() => {
}
.card-grid {
flex: 1;
min-height: 0;
overflow: auto;
padding: 16px;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;

View File

@ -528,20 +528,24 @@
.track-content-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
}
.track-content-page-card {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
@ -550,21 +554,24 @@
.search-input {
width: 20%;
max-width: 320px;
margin-bottom: 16px;
margin: 16px 16px 16px;
}
.table-toolbar {
margin-bottom: 8px;
padding: 0 16px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
}
.list-wrap {
width: 100%;
flex: 0 0 auto;
height: 1200px;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;

View File

@ -375,20 +375,24 @@
.track-flag-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
}
.track-flag-page-card {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
@ -397,21 +401,24 @@
.search-input {
width: 20%;
max-width: 320px;
margin-bottom: 16px;
margin: 16px 16px 16px;
}
.table-toolbar {
margin-bottom: 8px;
padding: 0 16px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
}
.list-wrap {
width: 100%;
flex: 0 0 auto;
height: 1200px;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;

View File

@ -2191,12 +2191,12 @@
<style scoped lang="less">
.track-order-container {
padding: 16px; // 符合设计规范
padding: 16px 16px 24px; // 底边 24px 与主内容区对齐
background-color: #f0f3f4; // 与主背景统一
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
width: 100%;
max-width: 100%;
box-sizing: border-box;
@ -2251,18 +2251,17 @@
/*
* 列表滚动相关(Handsontable 实际滚动发生在 .ht_master > .wtHolder):
* - 高度:useTableContainerHeight 读取 .list-wrap 的 clientHeight → :height="tableHeight",决定 wtHolder 可视区高度。
* - 布局:.table-section 固定 1230px + 纵向 flex;.list-wrap flex:1 + min-height:0;HandsontablePro 根节点 flex 填满。
* - 布局:.table-section flex:1 + min-height:0;.list-wrap flex:1 + min-height:0 + 四边 16px 内边距;HandsontablePro 根节点 flex 填满。
* - 外层 .track-order-container 为 overflow-y:auto 时,滚轮易冒泡到整页,体感像「拖不动」;对表区域加 overscroll-behavior:contain 优先把滚轮留在表内。
* - HandsontablePro 内 .ht_master 为 overflow:visible(克隆表头需要),横向/纵向滚动条样式写在 .wtHolder 上。
*/
.table-section {
background: #fff;
border-radius: 8px;
padding: 12px; // 减少padding,增加内容空间
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
height: 1230px;
flex: 0 0 auto;
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
@ -2273,8 +2272,11 @@
}
.list-wrap {
width: 100%;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden; // 仅裁剪外层,纵向/横向滚动交给 Handsontable .wtHolder
@ -3382,10 +3384,12 @@
.table-toolbar {
margin-bottom: 8px;
padding: 16px 16px 0;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
flex-shrink: 0;
.toolbar-left {

View File

@ -410,23 +410,28 @@
</template>
<style scoped lang="less">
/* 参与 page-layout 主内容区 flex,撑满高度;底部仅 24px 留白(列表容器随区域增高,不再出现大块灰底) */
.track-state-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
background-color: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
}
.track-state-page-card {
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
@ -435,21 +440,24 @@
.search-input {
width: 20%;
max-width: 320px;
margin-bottom: 16px;
margin: 16px 16px 16px;
}
.table-toolbar {
margin-bottom: 8px;
padding: 0 16px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
}
.list-wrap {
width: 100%;
flex: 0 0 auto;
height: 1200px;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;

View File

@ -513,23 +513,24 @@ const handsontableRef = ref<InstanceType<typeof HandsontablePro>>()
.customer-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
display: flex;
flex-direction: column;
background: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
}
.customer-page-card {
flex: 0 0 auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
overflow: hidden;
@ -539,8 +540,10 @@ const handsontableRef = ref<InstanceType<typeof HandsontablePro>>()
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
flex-wrap: nowrap;
.toolbar-left {
display: flex;
@ -559,9 +562,10 @@ const handsontableRef = ref<InstanceType<typeof HandsontablePro>>()
.list-wrap {
width: 100%;
min-width: 0;
flex: 0 0 auto;
height: 1200px;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;

View File

@ -234,23 +234,24 @@ function handleViewPayImageClick(event: MouseEvent) {
.recharge-record-page {
width: 100%;
box-sizing: border-box;
padding: 16px;
padding: 16px 16px 24px;
display: flex;
flex-direction: column;
background: #f0f3f4;
min-height: 100vh;
height: auto;
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
}
.recharge-record-page-card {
flex: 0 0 auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
padding: 16px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
overflow: hidden;
@ -260,8 +261,10 @@ function handleViewPayImageClick(event: MouseEvent) {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
flex-wrap: nowrap;
.toolbar-right {
.pagination-controls {
@ -274,9 +277,10 @@ function handleViewPayImageClick(event: MouseEvent) {
.list-wrap {
width: 100%;
min-width: 0;
flex: 0 0 auto;
height: 1200px;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;