feat: 同步地大功能至2026年10月20日
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,768 @@
|
||||
/* 人员数据治理列表统一占满内容区,数据超出时只滚动表格主体。 */
|
||||
.person-governance-page-transition {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.person-governance-page-transition.is-ready {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.person-governance-page-transition.is-leaving {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.person-governance-list-page {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.person-governance-search-card {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-list-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 会员匹配列表的查询区与列表区间距缩小为原来的一半。 */
|
||||
.person-governance-compact-gap .person-governance-list-card {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-list-card > .el-card__body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.person-governance-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-table-wrap > .el-table {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-list-footer,
|
||||
.person-governance-list-card .el-pagination-container {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 人员变更列表位于 Guava 默认插槽内,列表视图仍按查询区、表格区、分页区分配固定高度。 */
|
||||
.person-governance-change-page > .guava-main-content,
|
||||
.person-governance-change-page > .guava-main-content > .transition-item {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-change-page > .guava-main-content > .transition-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.person-governance-change-list-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.person-governance-change-list-card > .el-card__body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.person-governance-change-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-change-table-wrap > .el-table {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-change-list-card .el-pagination-container {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 配置页顶部字段根据内容区宽度自动分列,空间不足时自然换行。 */
|
||||
.person-governance-form-page {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* PageHeader 提供统一返回入口,并与基本配置保持紧凑间距。 */
|
||||
.person-governance-page-header {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 16px 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 表单页只保留返回入口,移除 PageHeader 空内容对应的分隔线和占位间距。 */
|
||||
.person-governance-page-header .el-page-header__left {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.person-governance-page-header .el-page-header__left::after,
|
||||
.person-governance-page-header .el-page-header__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.person-governance-form-basic-card {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 第三方接口按Token与数据分区管理,一级页签保持紧凑并独立于各自的子页签。 */
|
||||
.person-governance-endpoint-tabs-card {
|
||||
flex: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.person-governance-endpoint-tabs-card > .el-card__body {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.person-governance-endpoint-tabs-card .el-tabs__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.person-governance-endpoint-tabs-card .el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 顶部基本配置横向铺满卡片,并压缩默认表单项产生的底部空白。 */
|
||||
.person-governance-form-basic-card > .el-card__body {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.person-governance-form-basic-card .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 两类会员规则使用独立页签,页签本身不参与内容区滚动。 */
|
||||
.person-governance-member-type-tabs {
|
||||
flex: none;
|
||||
margin-top: 10px;
|
||||
padding: 0 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.person-governance-member-type-tabs .el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.person-governance-member-type-tabs .el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 主要配置板块占满剩余高度,内容过多时仅在板块内部滚动。 */
|
||||
.person-governance-form-content-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-form-content-card > .el-card__body {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row > .el-col,
|
||||
.person-governance-form-content-row > .el-col > .el-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row > .el-col {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row > .el-col > .el-card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row > .el-col > .el-card > .el-card__body {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 匹配规则标题固定,条件组占用剩余高度并独立滚动。 */
|
||||
.person-governance-form-content-row > .el-col > .person-governance-member-rules > .el-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-member-rules-header {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-member-rules-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* 会员预览固定标题和操作区,表格独占剩余高度,长列表只滚动表格内部。 */
|
||||
.person-governance-form-content-row > .el-col > .person-governance-member-preview > .el-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-member-preview h3,
|
||||
.person-governance-member-preview p,
|
||||
.person-governance-member-preview > .el-card__body > .el-button {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-member-preview > .el-card__body > .el-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.person-governance-member-preview-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.person-governance-member-preview-pagination {
|
||||
flex: none;
|
||||
padding-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.person-governance-member-preview-pagination .el-pagination {
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.person-governance-member-binding-hint {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.person-governance-member-condition-relation > td {
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 每层条件组对应一层括号语义,通过边框和缩进直观展示父子层级。 */
|
||||
.person-governance-member-group {
|
||||
margin-top: 20px;
|
||||
padding: 14px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.person-governance-member-group.is-nested {
|
||||
margin: 0 0 0 20px;
|
||||
border-color: #b3d8ff;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.person-governance-member-group-header {
|
||||
min-height: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.person-governance-member-group-actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-member-group-relation {
|
||||
margin: 28px 0;
|
||||
}
|
||||
|
||||
.person-governance-member-child-relation {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.person-governance-form-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
column-gap: 20px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-form-grid .el-form-item {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.person-governance-form-grid .el-select,
|
||||
.person-governance-form-grid .el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* API配置顶部字段按业务宽度靠左排列,同步方式具体值单独放在页首右侧。 */
|
||||
.person-governance-sync-type {
|
||||
color: #303133;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid {
|
||||
/* 按业务宽度紧凑靠左,剩余空间留在右侧;窄屏只允许整组字段换行。 */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px 16px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid > .el-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 280px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid .el-form-item__label {
|
||||
flex: none;
|
||||
float: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid .el-form-item__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid > .person-governance-api-request-address {
|
||||
flex-basis: 620px;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid > .person-governance-api-timeout {
|
||||
flex-basis: 270px;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid > .person-governance-query-action {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-request-address-control,
|
||||
.person-governance-timeout-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.person-governance-request-address-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-request-address-control .el-select {
|
||||
flex: none;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.person-governance-request-address-control > .el-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.person-governance-timeout-control .el-input-number {
|
||||
flex: none;
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.person-governance-timeout-unit {
|
||||
flex: none;
|
||||
margin-left: 8px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* API配置页签占满内容卡片,返回结果文本域随剩余高度伸展。 */
|
||||
.person-governance-api-content-card > .el-card__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-api-content-card > .el-card__body > .el-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-api-content-card > .el-card__body > .el-tabs > .el-tabs__header {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-api-content-card > .el-card__body > .el-tabs > .el-tabs__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.person-governance-api-content-card > .el-card__body > .el-tabs > .el-tabs__content > .el-tab-pane {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 系统表配置不显示单一的字段映射页签标题,映射内容直接占满卡片。 */
|
||||
.person-governance-system-table-card > .el-card__body > .el-tabs > .el-tabs__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.person-governance-system-table-card > .el-card__body > .el-tabs > .el-tabs__content,
|
||||
.person-governance-system-table-card .person-governance-mapping-pane {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-system-table-card .person-governance-mapping-pane {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.person-governance-result-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.person-governance-result-textarea {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 返回结果下方的映射配置固定保留,长响应只在文本域内滚动。 */
|
||||
.person-governance-response-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: none;
|
||||
gap: 10px 20px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.person-governance-response-fields > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 280px;
|
||||
min-width: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.person-governance-response-fields label,
|
||||
.person-governance-task-cron label {
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-response-fields .el-input,
|
||||
.person-governance-response-fields .el-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.person-governance-task-cron {
|
||||
flex: 0 1 420px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.person-governance-cron-hint,
|
||||
.person-governance-json-hint {
|
||||
flex: none;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.person-governance-parameter-pane .person-governance-body-json {
|
||||
flex: 1;
|
||||
min-height: 80px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.person-governance-result-textarea .el-textarea__inner,
|
||||
.person-governance-body-json .el-textarea__inner {
|
||||
height: 100%;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.person-governance-result-actions {
|
||||
flex: none;
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.person-governance-body-json {
|
||||
height: calc(100% - 52px);
|
||||
}
|
||||
|
||||
/* 字段映射与请求参数表格在可见页签内直接按完整宽高渲染。 */
|
||||
.person-governance-mapping-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-mapping-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-wrap > .el-table {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-mapping-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 24px;
|
||||
}
|
||||
|
||||
.person-governance-mapping-toolbar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 两种数据来源共用右侧清空入口,窄屏换行后仍靠右,不挤压周期控件。 */
|
||||
.person-governance-mapping-toolbar > .person-governance-clear-mappings {
|
||||
flex: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.person-governance-mapping-toolbar-item > label {
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
color: #303133;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-select {
|
||||
flex: 0 1 360px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-select .el-select {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.person-governance-mapping-switch {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 系统表配置项按配置名称、来源表、目标表、同步开关的顺序自适应排列。 */
|
||||
.person-governance-system-table-toolbar {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .el-form-item {
|
||||
/* 标签与控件作为一组排列,仅允许表单项在工具栏中整体换行。 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 280px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .person-governance-system-target-table {
|
||||
flex: 0 1 300px;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .person-governance-system-task-cron {
|
||||
flex: 0 1 430px;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .person-governance-system-member-match {
|
||||
flex: none;
|
||||
column-gap: 12px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
/* 长标签按文字实际宽度展示,通过固定列间距避免文字与开关重叠或距离过大。 */
|
||||
.person-governance-system-table-toolbar .person-governance-system-member-match .el-form-item__label {
|
||||
width: auto !important;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .person-governance-system-member-match .el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .el-form-item__label {
|
||||
flex: none;
|
||||
float: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .el-form-item__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .el-input,
|
||||
.person-governance-system-table-toolbar .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-parameter-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.person-governance-parameter-pane > .el-tabs,
|
||||
.person-governance-parameter-pane > .el-radio-group {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-parameter-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-parameter-table-wrap > .el-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 参数表头按文字与加号按钮的实际高度紧凑展示。 */
|
||||
.person-governance-parameter-table .el-table__header-wrapper th,
|
||||
.person-governance-mapping-table .el-table__header-wrapper th {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.person-governance-parameter-table .el-table__header-wrapper th .cell,
|
||||
.person-governance-mapping-table .el-table__header-wrapper th .cell {
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-governance-add-column .el-button {
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
/* 弹窗和抽屉沿用 Element UI 过渡,并为本模块调整为更柔和的缓动。 */
|
||||
.dialog-fade-enter-active .person-governance-dialog,
|
||||
.dialog-fade-leave-active .person-governance-dialog {
|
||||
transition:
|
||||
opacity 260ms ease,
|
||||
transform 260ms ease;
|
||||
}
|
||||
|
||||
.dialog-fade-enter .person-governance-dialog,
|
||||
.dialog-fade-leave-to .person-governance-dialog {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.99);
|
||||
}
|
||||
|
||||
/* 弹窗挂载body避开页面过渡层,短屏内部滚动,窄屏保留周期编辑器的完整操作区域。 */
|
||||
.person-governance-cron-dialog {
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
|
||||
.person-governance-cron-dialog .el-dialog__body {
|
||||
max-height: 78vh;
|
||||
overflow: auto;
|
||||
padding: 10px 20px 20px;
|
||||
}
|
||||
|
||||
.person-governance-cron-dialog .popup-main {
|
||||
min-width: 640px;
|
||||
}
|
||||
|
||||
.person-governance-drawer {
|
||||
transition: box-shadow 260ms ease;
|
||||
}
|
||||
|
||||
.person-governance-drawer.rtl,
|
||||
.el-drawer__open .person-governance-drawer.rtl {
|
||||
animation-duration: 340ms;
|
||||
animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.person-governance-list-page,
|
||||
.person-governance-form-page {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.person-governance-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.person-governance-page-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.person-governance-timeout-control .el-input-number {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-select {
|
||||
flex-basis: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ const initTableMixins = {
|
||||
pageData(data = null) {
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
// 返回请求 Promise,供页面在列表数据加载结束后再显示主体内容。
|
||||
return this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<van-action-sheet
|
||||
v-model="visible"
|
||||
<van-popup
|
||||
:value="visible"
|
||||
class="activity-schedule-calendar"
|
||||
:style="{ height: '78%' }"
|
||||
:close-on-click-overlay="true"
|
||||
title="活动日历"
|
||||
closeable>
|
||||
position="right"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false">
|
||||
<van-nav-bar title="课程日历" left-text="返回" left-arrow
|
||||
@click-left="closeSignPopup('calendar')"></van-nav-bar>
|
||||
<div class="activity-schedule-calendar__body">
|
||||
<div class="activity-schedule-calendar__month-bar">
|
||||
<button type="button" class="activity-schedule-calendar__month-btn" @click="changeMonth(-1)">
|
||||
@@ -30,7 +32,10 @@
|
||||
:class="{
|
||||
'is-other-month': !day.currentMonth,
|
||||
'is-selected': day.key === selectedDate,
|
||||
'is-today': day.isToday
|
||||
'is-today': day.isToday,
|
||||
'is-signed': day.status === 'signed',
|
||||
'is-available': day.status === 'available',
|
||||
'is-pending': day.status === 'pending'
|
||||
}"
|
||||
@click="selectDay(day)">
|
||||
<span>
|
||||
@@ -42,13 +47,11 @@
|
||||
</div>
|
||||
|
||||
<div class="activity-schedule-calendar__legend">
|
||||
<span><i class="is-solid"></i>已签到</span>
|
||||
<span><i class="is-outline"></i>可签到</span>
|
||||
<span><i class="is-muted"></i>未开始</span>
|
||||
<span><i class="is-signed"></i>已签到</span>
|
||||
<span><i class="is-available"></i>可签到</span>
|
||||
<span><i class="is-pending"></i>未开始</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showMap" :id="mapContainerId" class="activity-schedule-calendar__map"></div>
|
||||
|
||||
<section class="activity-schedule-calendar__schedule-card">
|
||||
<div class="activity-schedule-calendar__schedule-title">
|
||||
{{ selectedDate ? selectedDate + ' 的活动时段' : '活动时段' }}
|
||||
@@ -72,6 +75,7 @@
|
||||
v-else-if="canSignItem(item)"
|
||||
type="button"
|
||||
class="activity-schedule-calendar__sign-btn"
|
||||
:disabled="formLoading"
|
||||
@click.stop="onSign(item)">签到</button>
|
||||
<span v-else class="activity-schedule-calendar__status" :class="{
|
||||
'is-ended': getScheduleStatus(item) === '已结束',
|
||||
@@ -88,16 +92,53 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
</van-popup>
|
||||
|
||||
<van-popup
|
||||
v-model="signVisible"
|
||||
:value="positionConfirmVisible"
|
||||
class="activity-schedule-position-popup"
|
||||
position="right"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false"
|
||||
@opened="initMap"
|
||||
@closed="onPositionPopupClosed">
|
||||
<div class="activity-schedule-position">
|
||||
<van-nav-bar title="定位签到" left-text="返回" left-arrow
|
||||
@click-left="closeSignPopup('position-confirm')"></van-nav-bar>
|
||||
<div :id="mapContainerId" class="activity-schedule-calendar__map"></div>
|
||||
<div class="activity-schedule-position__details">
|
||||
<div class="activity-schedule-position__location-tools">
|
||||
<span class="activity-schedule-position__location-status" role="status">{{ locationMessage }}</span>
|
||||
<van-button size="small" plain type="info" icon="aim" :loading="locationLoading"
|
||||
:disabled="formLoading || !!closingPopupKey" @click="refreshLocation">重新定位</van-button>
|
||||
</div>
|
||||
<div class="activity-schedule-position__title">{{ row ? row.courseName : '' }}</div>
|
||||
<div v-if="selectCourseTime" class="activity-schedule-position__text">
|
||||
{{ dateKey(selectCourseTime) }} {{ formatTime(selectCourseTime.courseStartTime) }} - {{ formatTime(selectCourseTime.courseEndTime) }}
|
||||
</div>
|
||||
<div v-if="row && row.courseLocation" class="activity-schedule-position__text">
|
||||
<van-icon name="location-o"></van-icon>{{ row.courseLocation }}
|
||||
</div>
|
||||
<div class="activity-schedule-position__text">签到范围:{{ row ? (row.radius || 100) : 100 }} 米</div>
|
||||
</div>
|
||||
<div class="activity-schedule-position__footer">
|
||||
<van-button type="info" block round :loading="formLoading" :disabled="formLoading || locationLoading || !!closingPopupKey"
|
||||
loading-text="签到中..." @click="onConfirmPositionSign">确认签到</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-popup
|
||||
:value="signVisible"
|
||||
round
|
||||
closeable
|
||||
:safe-area-inset-bottom="true"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false"
|
||||
:style="{ width: '80%', height: '66%' }"
|
||||
get-container="#app"
|
||||
@click-close-icon="closeSignPopup('scan')"
|
||||
@close="onSignClose">
|
||||
<scan-code ref="scanCodeRef"></scan-code>
|
||||
</van-popup>
|
||||
@@ -126,7 +167,17 @@ module.exports = {
|
||||
weekdays: ["日", "一", "二", "三", "四", "五", "六"],
|
||||
selectCourseTime: null,
|
||||
signVisible: false,
|
||||
signCodePreview: null,
|
||||
positionConfirmVisible: false,
|
||||
formLoading: false,
|
||||
popupHistoryToken: "",
|
||||
calendarHistoryOpen: false,
|
||||
closingPopupKey: "",
|
||||
markerLayer: null,
|
||||
userMarkerLayer: null,
|
||||
locationLoading: false,
|
||||
locationMessage: "",
|
||||
locationRequestId: 0,
|
||||
map: null,
|
||||
circle: null,
|
||||
mapContainerId: "activityScheduleMap_" + Math.random().toString(36).slice(2)
|
||||
@@ -181,9 +232,11 @@ module.exports = {
|
||||
this.currentMonth = date.month() + 1
|
||||
this.selectedDate = initialDate
|
||||
this.visible = true
|
||||
this.$nextTick(() => {
|
||||
if (this.showMap) this.initMap()
|
||||
})
|
||||
// 所有签到模式的日历均入栈;上层扫码、二维码或定位弹框单独入栈,返回时逐层关闭。
|
||||
if (!this.calendarHistoryOpen) {
|
||||
this.calendarHistoryOpen = true
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "calendar")
|
||||
}
|
||||
},
|
||||
dateKey(item) {
|
||||
const value = item && (item.courseDate || item.courseStartTime)
|
||||
@@ -230,18 +283,43 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
onSign(courseTime) {
|
||||
// 确认框关闭或定位提交期间不再切换时段,避免重复打卡和签到对象错位。
|
||||
if (this.formLoading || this.positionConfirmVisible || this.signVisible || this.signCodePreview || this.closingPopupKey) return
|
||||
this.selectCourseTime = courseTime
|
||||
if (this.row.signType === 1) {
|
||||
this.signVisible = true
|
||||
this.$nextTick(() => this.$refs.scanCodeRef.init())
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "scan")
|
||||
this.$nextTick(() => {
|
||||
if (this.signVisible) this.$refs.scanCodeRef.init()
|
||||
})
|
||||
} else if (this.row.signType === 2) {
|
||||
this.makeCode()
|
||||
} else if (this.row.signType === 3 && this.moduleType === "brand") {
|
||||
this.positionSign()
|
||||
this.positionConfirmVisible = true
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "position-confirm")
|
||||
} else {
|
||||
this.$toast("此签到模式正在升级中")
|
||||
}
|
||||
},
|
||||
/**
|
||||
* popupKey 为 calendar(日历)、scan(扫码)、sign-code(二维码)或 position-confirm(定位确认);无返回值。
|
||||
* 仅请求关闭栈顶弹层,由全局历史回调同步显示状态,防止连续点击造成多次返回。
|
||||
*/
|
||||
closeSignPopup(popupKey) {
|
||||
if (this.closingPopupKey) return
|
||||
if (window.h5PopupHistory.close(this.popupHistoryToken, popupKey)) {
|
||||
this.closingPopupKey = popupKey
|
||||
}
|
||||
},
|
||||
// 预览定位不提交签到;点击确认时重新获取位置,失败时保留弹框和当前时段供重试。
|
||||
onConfirmPositionSign() {
|
||||
if (this.formLoading || this.locationLoading || this.closingPopupKey || !this.positionConfirmVisible) return
|
||||
this.positionSign()
|
||||
},
|
||||
// 退场动画完成后释放地图;快速重新打开时保留新弹框正在使用的实例。
|
||||
onPositionPopupClosed() {
|
||||
if (!this.positionConfirmVisible) this.destroyMap()
|
||||
},
|
||||
onSignClose() {
|
||||
if (this.$refs.scanCodeRef) this.$refs.scanCodeRef.closeScan()
|
||||
},
|
||||
@@ -250,90 +328,214 @@ module.exports = {
|
||||
? "/platform/family/mine/passiveScan"
|
||||
: "/platform/trainSignUp/mine/passiveScan"
|
||||
const content = jrQrcode.getQrBase64(baseUrl + "?id=" + this.selectCourseTime.id)
|
||||
vant.ImagePreview([content])
|
||||
// 二维码预览关闭时同步回退历史;历史回调先释放引用,避免再次触发返回。
|
||||
const preview = vant.ImagePreview({
|
||||
images: [content],
|
||||
closeOnPopstate: false,
|
||||
onClose: () => {
|
||||
if (this.signCodePreview === preview) this.closeSignPopup("sign-code")
|
||||
}
|
||||
})
|
||||
this.signCodePreview = preview
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "sign-code")
|
||||
},
|
||||
/**
|
||||
* 确认后提交所选时段的点位签到,无方法参数、无返回值。
|
||||
* 接口参数:timeId 为报名时段 ID,courseId 为课程 ID,lat/lng 为定位经纬度。
|
||||
* 返回对象的 code 为 0 表示成功,msg 为失败提示;定位或请求结束后释放提交状态。
|
||||
*/
|
||||
positionSign() {
|
||||
this.getLocation(coords => {
|
||||
if (!coords) return
|
||||
if (this.formLoading || this.locationLoading) return
|
||||
const courseTime = this.selectCourseTime
|
||||
const course = this.row
|
||||
const requestId = ++this.locationRequestId
|
||||
this.formLoading = true
|
||||
this.locationMessage = "正在获取当前位置..."
|
||||
this.getLocation((coords, errorMessage) => {
|
||||
// 定位失败或等待定位时已退出地图弹框,不再发起签到请求。
|
||||
if (!this.visible || !this.positionConfirmVisible || requestId !== this.locationRequestId || this.row !== course || this.selectCourseTime !== courseTime) {
|
||||
this.formLoading = false
|
||||
return
|
||||
}
|
||||
this.showCurrentLocation(coords, errorMessage)
|
||||
if (!coords) {
|
||||
this.formLoading = false
|
||||
return
|
||||
}
|
||||
// 地图展示使用转换后的坐标,签到请求仍传手机返回的原始 WGS-84 经纬度。
|
||||
this.$axios.post("/platform/trainSignUp/mine/positionSign", {
|
||||
timeId: this.selectCourseTime.id,
|
||||
courseId: this.row.id,
|
||||
timeId: courseTime.id,
|
||||
courseId: course.id,
|
||||
lat: coords.lat,
|
||||
lng: coords.lng
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.selectCourseTime.isAttend = true
|
||||
this.$set(courseTime, "isAttend", true)
|
||||
this.$toast("签到成功")
|
||||
// 签到完成后清理本组件的弹框历史,返回时不再出现已完成的确认框或日历。
|
||||
return window.h5PopupHistory.clear(this.popupHistoryToken)
|
||||
} else {
|
||||
this.$toast(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
/**
|
||||
* callback 接收浏览器 WGS-84 定位对象 {lat, lng, accuracy} 和可选失败提示;无返回值。
|
||||
* lat/lng 为纬度/经度,accuracy 为精度半径(米);失败时坐标为 null。
|
||||
* 预览和确认签到均重新定位,不使用上一次定位缓存,是否提交由调用方决定。
|
||||
*/
|
||||
getLocation(callback) {
|
||||
if (!navigator.geolocation) {
|
||||
this.$toast("当前浏览器不支持定位功能")
|
||||
callback(null)
|
||||
callback(null, "当前浏览器不支持定位功能")
|
||||
return
|
||||
}
|
||||
const loading = vant.Toast.loading({ message: "获取定位中...", forbidClick: false, duration: 0 })
|
||||
navigator.geolocation.getCurrentPosition(position => {
|
||||
loading.close()
|
||||
callback({ lat: position.coords.latitude, lng: position.coords.longitude })
|
||||
callback({ lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy })
|
||||
}, error => {
|
||||
loading.close()
|
||||
const messageMap = {
|
||||
1: "请允许浏览器获取位置信息",
|
||||
2: "无法获取当前位置",
|
||||
3: "定位超时,请重试"
|
||||
}
|
||||
this.$toast(messageMap[error.code] || "定位失败,请稍后重试")
|
||||
callback(null)
|
||||
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 })
|
||||
callback(null, messageMap[error.code] || "定位失败,请稍后重试")
|
||||
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 })
|
||||
},
|
||||
// 打开地图和点击重新定位只刷新预览;请求编号隔离已关闭或已重新打开的弹框。
|
||||
refreshLocation() {
|
||||
if (!this.map || !this.positionConfirmVisible || this.locationLoading || this.formLoading || this.closingPopupKey) return
|
||||
const requestId = ++this.locationRequestId
|
||||
this.locationLoading = true
|
||||
this.locationMessage = "正在获取当前位置..."
|
||||
this.getLocation((coords, errorMessage) => {
|
||||
if (requestId !== this.locationRequestId || !this.map || !this.positionConfirmVisible) return
|
||||
this.locationLoading = false
|
||||
this.showCurrentLocation(coords, errorMessage)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* coords 为手机原始定位对象,errorMessage 为失败提示;无返回值,不提交签到。
|
||||
* 国内坐标复用项目工具转换为腾讯地图使用的 GCJ-02,境外保留原坐标。
|
||||
* 定位失败时移除旧位置点,避免将过期点位误认为当前位置。
|
||||
*/
|
||||
showCurrentLocation(coords, errorMessage) {
|
||||
if (!coords) {
|
||||
if (this.userMarkerLayer) this.userMarkerLayer.setGeometries([])
|
||||
this.locationMessage = errorMessage || "暂未获取到当前位置"
|
||||
this.$toast(this.locationMessage)
|
||||
return
|
||||
}
|
||||
this.locationMessage = Number.isFinite(coords.accuracy)
|
||||
? "定位精度约 " + Math.ceil(coords.accuracy) + " 米"
|
||||
: "已显示我的位置"
|
||||
if (!this.map || !this.userMarkerLayer) return
|
||||
const converted = this.$coordinateUtil.outOfChina(coords.lng, coords.lat)
|
||||
? [coords.lat, coords.lng]
|
||||
: this.$coordinateUtil.wgs84ToGcj02(coords.lng, coords.lat)
|
||||
const userPosition = new TMap.LatLng(converted[0], converted[1])
|
||||
this.userMarkerLayer.setGeometries([{ id: "current-user", styleId: "user", position: userPosition }])
|
||||
const activityPosition = this.markerLayer.getGeometries()[0].position
|
||||
// 同时显示签到点和我的位置,并留出边距;两点重合时仍保留可辨识的地图范围。
|
||||
const latPadding = Math.max(Math.abs(activityPosition.getLat() - converted[0]) * 0.3, 0.0003)
|
||||
const lngPadding = Math.max(Math.abs(activityPosition.getLng() - converted[1]) * 0.3, 0.0003)
|
||||
const bounds = new TMap.LatLngBounds(
|
||||
new TMap.LatLng(Math.min(activityPosition.getLat(), converted[0]) - latPadding, Math.min(activityPosition.getLng(), converted[1]) - lngPadding),
|
||||
new TMap.LatLng(Math.max(activityPosition.getLat(), converted[0]) + latPadding, Math.max(activityPosition.getLng(), converted[1]) + lngPadding)
|
||||
)
|
||||
this.map.fitBounds(bounds)
|
||||
},
|
||||
// 仅在侧滑弹框完全展开后创建地图,保证容器已有可用尺寸,避免动画期间地图空白。
|
||||
initMap() {
|
||||
if (!this.row.courseLocationCoordinates || typeof TMap === "undefined") return
|
||||
if (!this.positionConfirmVisible || !this.showMap || !this.row.courseLocationCoordinates || typeof TMap === "undefined") return
|
||||
const position = JSON.parse(this.row.courseLocationCoordinates)
|
||||
const center = new TMap.LatLng(position[0], position[1])
|
||||
if (this.map) this.map.destroy()
|
||||
this.destroyMap()
|
||||
this.map = new TMap.Map(this.mapContainerId, { zoom: 16, center })
|
||||
this.markerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
geometries: [{ id: "activity", position: center }]
|
||||
})
|
||||
// 蓝点与“我的位置”标签独立于红色签到点;内嵌 SVG 不依赖外部图片请求。
|
||||
const userIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="80" height="48" viewBox="0 0 80 48">'
|
||||
+ '<rect x="2" y="1" width="76" height="22" rx="11" fill="#1989fa"/>'
|
||||
+ '<text x="40" y="16" text-anchor="middle" font-family="sans-serif" font-size="12" fill="white">我的位置</text>'
|
||||
+ '<circle cx="40" cy="36" r="11" fill="#1989fa" fill-opacity="0.2"/>'
|
||||
+ '<circle cx="40" cy="36" r="6" fill="#1989fa" stroke="white" stroke-width="2"/></svg>'
|
||||
this.userMarkerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
zIndex: 2,
|
||||
styles: { user: new TMap.MarkerStyle({ width: 80, height: 48, anchor: { x: 40, y: 36 }, src: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(userIcon) }) },
|
||||
geometries: []
|
||||
})
|
||||
this.circle = new TMap.MultiCircle({
|
||||
map: this.map,
|
||||
geometries: [{ center, radius: this.row.radius || 100 }]
|
||||
})
|
||||
this.refreshLocation()
|
||||
},
|
||||
// 关闭弹框或销毁组件时释放地图及覆盖物引用,避免重复打开后残留地图实例。
|
||||
destroyMap() {
|
||||
// 使尚未返回的定位请求失效,避免旧结果写入下一次打开的地图。
|
||||
this.locationRequestId += 1
|
||||
this.locationLoading = false
|
||||
this.locationMessage = ""
|
||||
if (this.map) this.map.destroy()
|
||||
this.map = null
|
||||
this.markerLayer = null
|
||||
this.userMarkerLayer = null
|
||||
this.circle = null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 复用布局已加载的全局历史管理器,不在组件内注册 popstate 事件。
|
||||
this.popupHistoryToken = window.h5PopupHistory.register((popupKey) => {
|
||||
this.closingPopupKey = ""
|
||||
if (popupKey === "calendar") {
|
||||
this.calendarHistoryOpen = false
|
||||
this.visible = false
|
||||
} else if (popupKey === "scan") {
|
||||
this.signVisible = false
|
||||
} else if (popupKey === "sign-code") {
|
||||
const preview = this.signCodePreview
|
||||
this.signCodePreview = null
|
||||
if (preview) preview.close()
|
||||
} else if (popupKey === "position-confirm") {
|
||||
this.positionConfirmVisible = false
|
||||
this.locationRequestId += 1
|
||||
this.locationLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 页面离开时停止扫码并释放地图,再清理历史并注销回调,避免摄像头继续占用。
|
||||
if (this.signVisible) this.onSignClose()
|
||||
this.positionConfirmVisible = false
|
||||
this.destroyMap()
|
||||
window.h5PopupHistory.clear(this.popupHistoryToken).then(() => {
|
||||
window.h5PopupHistory.unregister(this.popupHistoryToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-schedule-calendar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 18px 18px 0 0;
|
||||
background: #f4f7fc;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar /deep/ .van-action-sheet__header {
|
||||
height: 50px;
|
||||
color: #1f2f43;
|
||||
background: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar /deep/ .van-action-sheet__close {
|
||||
top: 14px;
|
||||
color: #7e899a;
|
||||
font-size: 21px;
|
||||
.activity-schedule-calendar > .van-nav-bar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__body {
|
||||
height: 100%;
|
||||
padding: 14px 14px 24px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 18px 14px calc(24px + env(safe-area-inset-bottom));
|
||||
overflow-y: auto;
|
||||
overflow-anchor: none;
|
||||
box-sizing: border-box;
|
||||
@@ -383,8 +585,9 @@ module.exports = {
|
||||
.activity-schedule-calendar__day {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 38px;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
max-width: 44px;
|
||||
height: 52px;
|
||||
margin: 1px auto;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
@@ -400,13 +603,13 @@ module.exports = {
|
||||
|
||||
.activity-schedule-calendar__day > span {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
@@ -436,15 +639,27 @@ module.exports = {
|
||||
color: #bdc5d0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-signed > span {
|
||||
color: #148a52;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-available > span {
|
||||
color: #1675d1;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-pending > span {
|
||||
color: #66758a;
|
||||
}
|
||||
|
||||
/* 选中蓝底与外层状态环独立:切换日期时仍能分辨已签到、可签到和未开始。 */
|
||||
.activity-schedule-calendar__day.is-selected > span {
|
||||
flex: 0 0 40px;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
color: #fff;
|
||||
background: #1989fa;
|
||||
box-shadow: 0 5px 12px rgba(25, 137, 250, .22);
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-today .activity-schedule-calendar__day-number {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
|
||||
@@ -454,60 +669,150 @@ module.exports = {
|
||||
|
||||
.activity-schedule-calendar__day-status {
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
transform: translateX(-50%);
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-signed {
|
||||
background: #1989fa;
|
||||
.activity-schedule-calendar__day-status.is-signed,
|
||||
.activity-schedule-calendar__legend .is-signed {
|
||||
border: 2px solid #19a965;
|
||||
background: #eaf8f1;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-available {
|
||||
border: 1px solid #1989fa;
|
||||
.activity-schedule-calendar__day-status.is-available,
|
||||
.activity-schedule-calendar__legend .is-available {
|
||||
border: 2px solid #1989fa;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-pending {
|
||||
background: #d8dde5;
|
||||
.activity-schedule-calendar__day-status.is-pending,
|
||||
.activity-schedule-calendar__legend .is-pending {
|
||||
border: 2px dashed #98a4b5;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend {
|
||||
display: flex;
|
||||
margin: 12px 4px 16px;
|
||||
margin: 16px 0 20px;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
color: #78869a;
|
||||
font-size: 11px;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
color: #58677b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend span {
|
||||
display: inline-flex;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend i {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend .is-solid { background: #1989fa; }
|
||||
.activity-schedule-calendar__legend .is-outline { border: 1px solid #1989fa; }
|
||||
.activity-schedule-calendar__legend .is-muted { background: #d8dde5; }
|
||||
/* 小屏幕每列不足 42px 时缩小圆环,保留日期间距和选中态的白色间隔。 */
|
||||
@media (max-width: 350px) {
|
||||
.activity-schedule-calendar__day-status {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day > span {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__today-label,
|
||||
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-schedule-position-popup {
|
||||
background: #f4f7fc;
|
||||
}
|
||||
|
||||
.activity-schedule-position {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.activity-schedule-position > .van-nav-bar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__map {
|
||||
flex: 1 0 240px;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
margin-bottom: 12px;
|
||||
min-height: 240px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__details {
|
||||
flex-shrink: 0;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-position__title {
|
||||
color: #263548;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-tools {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-tools .van-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-status {
|
||||
color: #78869a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__text {
|
||||
margin-top: 6px;
|
||||
color: #78869a;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.activity-schedule-position__text .van-icon {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.activity-schedule-position__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-card {
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
placeholder="请输入关键词查询地址"
|
||||
@select="handleSelect"
|
||||
:trigger-on-focus="false"
|
||||
:disabled="!map"
|
||||
/>
|
||||
</div>
|
||||
<div id="mapContainer"></div>
|
||||
<el-alert v-if="mapError" :title="mapError" type="error" :closable="false" show-icon></el-alert>
|
||||
<div :id="mapContainerId" class="tmap-container" v-loading="mapLoading"></div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,9 +36,12 @@ module.exports = {
|
||||
data() {
|
||||
return {
|
||||
map: null,
|
||||
// Vue 实例编号区分同页多个地图,避免地图挂到其他弹窗的容器中。
|
||||
mapContainerId: 'tmap-container-' + this._uid,
|
||||
mapLoading: false,
|
||||
mapError: '',
|
||||
mapDisposed: false,
|
||||
poi: this.position,
|
||||
appMapCenterPointX: 0,
|
||||
appMapCenterPointY: 0,
|
||||
markerLayer: null,
|
||||
addressValue: '',
|
||||
circle: null,
|
||||
@@ -43,7 +49,7 @@ module.exports = {
|
||||
},
|
||||
methods: {
|
||||
handleSelect(item) {
|
||||
if (item.location) {
|
||||
if (this.map && item.location) {
|
||||
// 定位到中心点
|
||||
const center = new TMap.LatLng(item.location.lat, item.location.lng)
|
||||
this.map.setCenter(center)
|
||||
@@ -52,41 +58,84 @@ module.exports = {
|
||||
this.poi = [item.location.lat, item.location.lng]
|
||||
}
|
||||
},
|
||||
async querySearchAsync(queryString, cb) {
|
||||
// queryString 为用户输入的地址关键词;接口 data.data 是地点数组,cb 接收带 value 的候选项数组。
|
||||
querySearchAsync(queryString, cb) {
|
||||
if (!queryString) {
|
||||
cb([])
|
||||
return
|
||||
}
|
||||
const res = await this.$axios.post("/platform/TMap/suggestion", {
|
||||
return this.$axios.post("/platform/TMap/suggestion", {
|
||||
keyword: queryString,
|
||||
}).then((res) => {
|
||||
if (this.mapDisposed) return
|
||||
if (res.code === 0 && res.data.status === 0) {
|
||||
res.data.data.forEach((item) => {
|
||||
item.value = item.title + "【详细地址:" + item.address + "】"
|
||||
})
|
||||
cb(res.data.data)
|
||||
} else {
|
||||
this.$message.error((res.data && res.data.message) || res.msg || '地点搜索失败')
|
||||
cb([])
|
||||
}
|
||||
}).catch(() => {
|
||||
if (!this.mapDisposed) cb([])
|
||||
})
|
||||
if (res.code === 0 && res.data.status === 0) {
|
||||
res.data.data.forEach((item) => {
|
||||
item.value = item.title + "【详细地址:" + item.address + "】"
|
||||
})
|
||||
cb(res.data.data)
|
||||
} else {
|
||||
this.$message.error(res.msg)
|
||||
cb([])
|
||||
}
|
||||
},
|
||||
initMap() {
|
||||
const center = new TMap.LatLng(this.appMapCenterPointY, this.appMapCenterPointX)
|
||||
this.map = new TMap.Map('mapContainer', {
|
||||
resizeEnable: true,
|
||||
// 接收 [纬度, 经度],返回数值坐标或 null;拒绝空配置,防止空字符串被当成 0。
|
||||
normalizePosition(position) {
|
||||
if (!Array.isArray(position) || position.length !== 2) return null
|
||||
if (position.some(value => !['number', 'string'].includes(typeof value) || String(value).trim() === '')) return null
|
||||
const point = position.map(value => Number(value))
|
||||
if (!point.every(value => Number.isFinite(value)) || Math.abs(point[0]) > 90 || Math.abs(point[1]) > 180) return null
|
||||
return point
|
||||
},
|
||||
// 已设置的签到点直接用于回显;仅无有效点位时读取默认中心,避免配置请求阻断已有点位。
|
||||
loadMap() {
|
||||
if (this.mapDisposed || this.map || this.mapLoading) return
|
||||
this.mapLoading = true
|
||||
this.mapError = ''
|
||||
return Promise.resolve().then(() => {
|
||||
if (this.mapDisposed) return null
|
||||
if (typeof TMap === 'undefined' || !TMap.Map || !TMap.LatLng) {
|
||||
throw new Error('腾讯地图脚本未加载成功,请刷新页面后重试')
|
||||
}
|
||||
const point = this.normalizePosition(this.poi)
|
||||
if (point) return point
|
||||
return Promise.all([
|
||||
this.getConfigKey('AppMapCenterPointY'),
|
||||
this.getConfigKey('AppMapCenterPointX')
|
||||
]).then(values => {
|
||||
const center = this.normalizePosition(values)
|
||||
if (!center) throw new Error('地图默认中心未配置或无效,请检查系统配置 AppMapCenterPointX(经度)、AppMapCenterPointY(纬度)')
|
||||
return center
|
||||
})
|
||||
}).then(point => {
|
||||
// 弹窗关闭后可能仍收到配置响应,禁止在已销毁的容器中创建地图。
|
||||
if (!this.mapDisposed && point) this.initMap(point)
|
||||
}).catch(error => {
|
||||
if (this.mapDisposed) return
|
||||
this.mapError = (error && (error.message || error.msg)) || '地图初始化失败,请刷新页面后重试'
|
||||
this.destroyMap()
|
||||
}).finally(() => {
|
||||
if (!this.mapDisposed) this.mapLoading = false
|
||||
})
|
||||
},
|
||||
// point 为经过校验的 [纬度, 经度];只设置显示中心,不将默认中心自动保存为签到点。
|
||||
initMap(point) {
|
||||
const center = new TMap.LatLng(point[0], point[1])
|
||||
this.map = new TMap.Map(this.mapContainerId, {
|
||||
zoom: 16,
|
||||
center: center
|
||||
})
|
||||
// 初始化
|
||||
// 标记和签到半径共用同一个中心,点击选点后向表单回传腾讯地图坐标。
|
||||
this.markerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
geometries: []
|
||||
});
|
||||
if (this.poi && this.poi.length > 0) {
|
||||
const posi = new TMap.LatLng(this.poi[0], this.poi[1])
|
||||
this.createMarker(posi)
|
||||
this.map.setCenter(posi)
|
||||
} else {
|
||||
this.createMarker(center)
|
||||
// 默认中心仅用于浏览地图;只有已配置的有效坐标才回显签到标记和范围圈。
|
||||
const selectedPoint = this.normalizePosition(this.poi)
|
||||
if (selectedPoint) {
|
||||
this.createMarker(new TMap.LatLng(selectedPoint[0], selectedPoint[1]))
|
||||
}
|
||||
this.map.on("click", (event) => {
|
||||
if (!this.view) {
|
||||
@@ -113,17 +162,24 @@ module.exports = {
|
||||
position: position
|
||||
}
|
||||
]);
|
||||
this.circle = new TMap.MultiCircle({
|
||||
map: this.map,
|
||||
geometries: [{
|
||||
center: position,
|
||||
radius: this.radius,
|
||||
}],
|
||||
});
|
||||
const geometries = [{center: position, radius: this.radius}]
|
||||
// 重复选点复用半径图层,避免每次点击遗留一个空图层。
|
||||
if (this.circle) {
|
||||
this.circle.setGeometries(geometries)
|
||||
} else {
|
||||
this.circle = new TMap.MultiCircle({map: this.map, geometries})
|
||||
}
|
||||
},
|
||||
async getConfigKey(key) {
|
||||
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
|
||||
return resp.data
|
||||
// key 传系统配置名 AppMapCenterPointX/Y;返回 Promise,值为 Result.data 中的经纬度配置字符串。
|
||||
getConfigKey(key) {
|
||||
return this.$axios.post("/open/common/getConfigKey", { key }).then(resp => resp.data)
|
||||
},
|
||||
// 弹窗关闭或初始化失败时释放地图及其图层、事件和 WebGL 资源。
|
||||
destroyMap() {
|
||||
if (this.map) this.map.destroy()
|
||||
this.map = null
|
||||
this.markerLayer = null
|
||||
this.circle = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -134,19 +190,19 @@ module.exports = {
|
||||
this.$emit("update:position", newVal)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// DOM初始化完成进行地图初始化
|
||||
this.$nextTick(async () => {
|
||||
this.appMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
|
||||
this.appMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
|
||||
this.initMap()
|
||||
})
|
||||
mounted() {
|
||||
// 父弹窗显示完成后挂载组件,再等待当前容器完成渲染。
|
||||
this.$nextTick(() => this.loadMap())
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.mapDisposed = true
|
||||
this.destroyMap()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#mapContainer {
|
||||
.tmap-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
|
||||