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, "useSwipe": true,
"useSystemConfig": true, "useSystemConfig": true,
"useTabBarStore": true, "useTabBarStore": true,
"useTableContainerHeight": true,
"useTemplateRef": true, "useTemplateRef": true,
"useTemplateRefsList": true, "useTemplateRefsList": true,
"useTextDirection": true, "useTextDirection": true,

View File

@ -43,74 +43,6 @@ OMS(Order Management System,订单管理系统)作为独立服务运行和
4. **出库扫描**:订单发货时完成**出库扫描**,进入待发货/运输中。 4. **出库扫描**:订单发货时完成**出库扫描**,进入待发货/运输中。
5. **闭环完成**:出库扫描完成后,订单进入运输、派送环节,正常订单流程闭环结束。 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; // 超大屏 @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 容器类 ### 13.2 容器类
- 页面根容器:`xxx-container`(如 `role-page`、业务相关命名),保证 `padding: 16px` - 页面根容器:`xxx-container` / `xxx-page`(如 `role-page`、`track-order-container`),**列表类页面**推荐 `padding: 16px 16px 24px`,详见 **§2.5 页面壳、白卡片与列表区尺寸**
- 表格工具栏:`table-toolbar`、`toolbar-left`、`toolbar-right` - 白卡片:`*-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 按钮间距 ### 13.3 按钮间距
@ -438,8 +492,10 @@ const { t } = useI18n();
## 十四、检查清单(新页面开发) ## 十四、检查清单(新页面开发)
- [ ] 使用 `xxx-container` 或等效布局结构 - [ ] 使用 `xxx-container` / `xxx-page` 或等效布局结构;列表页见 **§2.5**:灰底 **`padding: 16px 16px 24px`**、`flex: 1; min-height: 0`
- [ ] 工具栏使用 `table-toolbar`、`toolbar-left`、`toolbar-right` - [ ] 白卡片与 **`.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` - [ ] 使用主题色变量,不硬编码 `#2D8C8C`
- [ ] 弹窗 / 抽屉使用 ModalPro / DrawerPro + useModal - [ ] 弹窗 / 抽屉使用 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 useSwipe: typeof import('@vueuse/core')['useSwipe']
const useSystemConfig: typeof import('./composables/useSystemConfig')['useSystemConfig'] const useSystemConfig: typeof import('./composables/useSystemConfig')['useSystemConfig']
const useTabBarStore: typeof import('./store/index')['useTabBarStore'] const useTabBarStore: typeof import('./store/index')['useTabBarStore']
const useTableContainerHeight: typeof import('./composables/useTableContainerHeight')['useTableContainerHeight']
const useTemplateRef: typeof import('vue')['useTemplateRef'] const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
@ -561,6 +562,7 @@ declare module 'vue' {
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']> readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useSystemConfig: UnwrapRef<typeof import('./composables/useSystemConfig')['useSystemConfig']> readonly useSystemConfig: UnwrapRef<typeof import('./composables/useSystemConfig')['useSystemConfig']>
readonly useTabBarStore: UnwrapRef<typeof import('./store/index')['useTabBarStore']> readonly useTabBarStore: UnwrapRef<typeof import('./store/index')['useTabBarStore']>
readonly useTableContainerHeight: UnwrapRef<typeof import('./composables/useTableContainerHeight')['useTableContainerHeight']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']> readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']> readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']> readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>

View File

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

View File

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

View File

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

View File

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

View File

@ -109,6 +109,16 @@ export default {
'menu.api.apiDebugger': 'API Debugger', 'menu.api.apiDebugger': 'API Debugger',
'menu.api.apiLike': 'API LIKE', 'menu.api.apiLike': 'API LIKE',
'menu.api.apiTest': 'API Test', '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': 'System',
'menu.system.org': 'Organization', 'menu.system.org': 'Organization',
'menu.system.organization': 'Organization', 'menu.system.organization': 'Organization',

View File

@ -109,6 +109,16 @@ export default {
'menu.api.apiDebugger': 'API调试', 'menu.api.apiDebugger': 'API调试',
'menu.api.apiLike': 'API LIKE', 'menu.api.apiLike': 'API LIKE',
'menu.api.apiTest': 'API测试', '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': '系统',
'menu.system.org': '组织管理', 'menu.system.org': '组织管理',
'menu.system.organization': '组织管理', 'menu.system.organization': '组织管理',

View File

@ -6,14 +6,16 @@
import HandsontablePro from '@/components/HandsontablePro/index.vue'; import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue'; import PaginationPro from '@/components/PaginationPro/index.vue';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'; import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
import { useI18n } from 'vue-i18n';
defineOptions({ name: 'CallStatistics' }); defineOptions({ name: 'CallStatistics' });
const { t } = useI18n();
const activeName = ref('1'); // 1:应用,2:服务商 const activeName = ref('1'); // 1:应用,2:服务商
const { tableContainerRef, tableHeight } = useTableContainerHeight(); const { tableContainerRef, tableHeight } = useTableContainerHeight();
const tabList = [ const tabList = [
{ key: '1', label: '应用' }, { key: '1', label: t('system.log.options.app') },
{ key: '2', label: '服务商' }, { key: '2', label: t('system.log.options.provider') },
]; ];
const tableData = ref<any[]>([]); const tableData = ref<any[]>([]);
@ -30,16 +32,16 @@
}); });
const typeParam = computed(() => (activeName.value === '2' ? 2 : 1)); 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(() => [ 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: 'name', label: nameLabel.value, width: 180, searchable: true, searchField: 'name' },
{ key: 'interfaceName', label: '接口名称', width: 180, searchable: true, searchField: 'interfaceName' }, { key: 'interfaceName', label: t('api.callStatistics.table.interfaceName'), width: 180, searchable: true, searchField: 'interfaceName' },
{ key: 'total', label: '总调用次数', width: 120, searchable: false }, { key: 'total', label: t('api.callStatistics.table.total'), width: 120, searchable: false },
{ key: 'succeedCount', label: '成功调用次数', width: 140, searchable: false }, { key: 'succeedCount', label: t('api.callStatistics.table.succeedCount'), width: 140, searchable: false },
{ key: 'failCount', label: '失败调用次数', width: 140, searchable: false }, { key: 'failCount', label: t('api.callStatistics.table.failCount'), width: 140, searchable: false },
{ key: 'successRate', label: '成功率', width: 100, searchable: false }, { key: 'successRate', label: t('api.callStatistics.table.successRate'), width: 100, searchable: false },
]); ]);
const processedTableData = computed(() => const processedTableData = computed(() =>
@ -121,7 +123,8 @@
</script> </script>
<template> <template>
<div class="call-statistics-container"> <div class="callStatistics-container">
<div class="callStatistics-page-card">
<!-- 标签页导航 --> <!-- 标签页导航 -->
<div class="tabs-section"> <div class="tabs-section">
<div class="tabs-container"> <div class="tabs-container">
@ -161,7 +164,7 @@
/> />
<a-input <a-input
v-model="searchParams.interfaceName" v-model="searchParams.interfaceName"
placeholder="接口名称" :placeholder="t('system.log.placeholder.interfaceName')"
class="filter-input" class="filter-input"
style="width: 160px" style="width: 160px"
allow-clear allow-clear
@ -184,7 +187,7 @@
</div> </div>
<!-- Handsontable 表格 --> <!-- Handsontable 表格 -->
<div ref="tableContainerRef" class="table-wrapper"> <div ref="tableContainerRef" class="list-wrap">
<HandsontablePro <HandsontablePro
:key="`table-${activeName}-${currentPage}`" :key="`table-${activeName}-${currentPage}`"
:columns="columns" :columns="columns"
@ -202,24 +205,40 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.call-statistics-container { .callStatistics-container {
padding: 16px; padding: 16px 16px 24px;
background-color: #f0f3f4; background-color: #f0f3f4;
height: 100vh; flex: 1;
min-height: 0;
overflow: hidden; overflow: hidden;
width: 100%; 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 { .tabs-section {
background: #fff; padding: 16px 16px 0;
border-radius: 8px; margin-bottom: 8px;
padding: 0 24px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex-shrink: 0; flex-shrink: 0;
background: transparent;
box-shadow: none;
border-radius: 0;
} }
.tabs-container { .tabs-container {
@ -255,18 +274,20 @@
} }
.table-section { .table-section {
background: #fff; background: transparent;
border-radius: 8px; border-radius: 0;
padding: 12px; padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: none;
height: calc(100vh - 80px); flex: 1;
overflow: visible; min-height: 0;
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
} }
.table-toolbar { .table-toolbar {
padding: 16px 16px 0;
margin-bottom: 8px; margin-bottom: 8px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -305,8 +326,26 @@
.table-wrapper { .table-wrapper {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: auto; overflow: hidden;
width: 100%; width: 100%;
height: 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> </style>

View File

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

View File

@ -1,7 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { computed, nextTick, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { $baseMessage } from '@/utils' 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({ defineOptions({
name: 'AutoTrigger' name: 'AutoTrigger'
@ -9,8 +15,20 @@ defineOptions({
const { t } = useI18n() const { t } = useI18n()
// 表格数据 type AutoTriggerRow = {
const tableData = ref([ id: number
status: boolean
operation: string
triggerTiming: string
triggerCondition: string
promptMessage: string
notifier: string
createTime: string
creator: string
}
// 表格数据(模拟)
const tableData = ref<AutoTriggerRow[]>([
{ {
id: 1, id: 1,
status: true, status: true,
@ -49,27 +67,221 @@ const tableData = ref([
// 分页相关 // 分页相关
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(10) 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 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') $baseMessage(t('channel.autoTrigger.message.deletePending'), 'info')
} }
// 编辑 type ModalMode = 'add' | 'edit'
const handleEdit = (row: any) => { const modalMode = ref<ModalMode>('add')
$baseMessage(t('channel.autoTrigger.message.editPending'), 'info') 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)
} }
// 新增 async function openEditModal(row: AutoTriggerRow) {
const handleAdd = () => { modalMode.value = 'edit'
$baseMessage(t('channel.autoTrigger.message.addPending'), 'info') 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 pageSize.value = size
currentPage.value = 1 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> </script>
<style lang="less" scoped> <style lang="less" scoped>
.auto-trigger-layout { .autoTrigger-container {
display: flex; width: 100%;
height: 100%; box-sizing: border-box;
padding: 16px; padding: 16px 16px 24px;
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;
display: flex; display: flex;
flex-direction: column; 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; overflow: hidden;
padding: 20px;
} }
.table-toolbar { .table-toolbar {
padding: 16px 16px 0;
margin-bottom: 16px; margin-bottom: 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-shrink: 0;
flex-wrap: nowrap;
.toolbar-left { .toolbar-left {
flex: 1; flex: 1;
@ -229,6 +550,28 @@ const handlePageSizeChange = (size: number) => {
position: relative; 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 { .custom-table-header {
background: #f7f8fa; background: #f7f8fa;
border-bottom: 1px solid #e5e6eb; border-bottom: 1px solid #e5e6eb;
@ -431,8 +774,12 @@ const handlePageSizeChange = (size: number) => {
} }
.empty-state { .empty-state {
padding: 40px 0; flex: 1;
text-align: center; 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; padding: 2px 8px;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
}
&.intercept { // HandsontablePro 单元格使用 innerHTML 注入,需用 :deep 让样式穿透 scoped
:deep(.operation-tag.intercept) {
background: #ffe7ba; background: #ffe7ba;
color: #d46b08; color: #d46b08;
} }
&.warning { :deep(.operation-tag.warning) {
background: #fff2e8; background: #fff2e8;
color: #d4380d; color: #d4380d;
} }
&.problem { :deep(.operation-tag.problem) {
background: #fff1f0; background: #fff1f0;
color: #cf1322; color: #cf1322;
} }
}
</style> </style>
<template> <template>
<div class="auto-trigger-layout"> <div class="autoTrigger-container">
<div class="right-content content-panel"> <div class="autoTrigger-page-card">
<!-- 表格操作按钮和分页控制 --> <!-- 表格操作按钮和分页控制 -->
<div class="table-toolbar"> <div class="table-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
@ -518,115 +866,53 @@ const handlePageSizeChange = (size: number) => {
</div> </div>
</div> </div>
<!-- 自定义表格 --> <!-- HandsontablePro 列表区 -->
<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"> <div v-if="!tableData.length" class="empty-state">
<a-empty :description="t('channel.autoTrigger.empty')" /> <a-empty :description="t('channel.autoTrigger.empty')" />
</div> </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> </div>
</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>
</div> </div>
</template> </template>

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="label-design-page"> <div class="labelDesign-container">
<div class="labelDesign-page-card">
<FullDesigner <FullDesigner
ref="designerRef" ref="designerRef"
default-lang="cn" default-lang="cn"
@ -12,6 +13,7 @@
<template #headerRight></template> <template #headerRight></template>
</FullDesigner> </FullDesigner>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -43,35 +45,57 @@ function onSave(payload: { template: unknown; data: unknown }) {
</script> </script>
<style scoped> <style scoped>
.label-design-page { .labelDesign-container {
padding: 16px; padding: 16px 16px 24px;
background-color: var(--color-bg-1); background-color: #f0f3f4;
min-height: calc(100vh - 86px); 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) */ /* 顶部栏背景透明(第一行 .primary-toolbar + 第二行 .secondary-toolbar) */
.label-design-page :deep(.primary-toolbar), .labelDesign-page-card :deep(.primary-toolbar),
.label-design-page :deep(.primary-toolbar .toolbar-content), .labelDesign-page-card :deep(.primary-toolbar .toolbar-content),
.label-design-page :deep(.secondary-toolbar), .labelDesign-page-card :deep(.secondary-toolbar),
.label-design-page :deep(.secondary-toolbar .toolbar-content) { .labelDesign-page-card :deep(.secondary-toolbar .toolbar-content) {
background: transparent !important; background: transparent !important;
background-color: transparent !important; background-color: transparent !important;
} }
/* 顶部中间/右侧区域也透明 */ /* 顶部中间/右侧区域也透明 */
.label-design-page :deep(.header-center), .labelDesign-page-card :deep(.header-center),
.label-design-page :deep(.header-right.toolbar-right) { .labelDesign-page-card :deep(.header-right.toolbar-right) {
background: transparent !important; background: transparent !important;
background-color: transparent !important; background-color: transparent !important;
} }
/* 隐藏 vg-print 顶部左侧 logo + 文本(打包代码中为 .header-left / .toolbar-logo / .toolbar-title) */ /* 隐藏 vg-print 顶部左侧 logo + 文本(打包代码中为 .header-left / .toolbar-logo / .toolbar-title) */
.label-design-page :deep(.header-left) { .labelDesign-page-card :deep(.header-left) {
display: none !important; display: none !important;
} }
/* 隐藏 vg-print 内置的反馈/帮助按钮(class: btn-feedback) */ /* 隐藏 vg-print 内置的反馈/帮助按钮(class: btn-feedback) */
.label-design-page :deep(.btn-feedback) { .labelDesign-page-card :deep(.btn-feedback) {
display: none !important; 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> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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