批量推送代码
This commit is contained in:
@@ -63,6 +63,7 @@ bin/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
*.zip
|
||||
.gradle/
|
||||
*.iml
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,161 +0,0 @@
|
||||
<template>
|
||||
<div class="gr-table-container">
|
||||
<div class="gr-table-filter">
|
||||
<div class="gr-query-wrap">
|
||||
<div>筛选</div>
|
||||
<van-field class="gr-query" v-model="queryVal"></van-field>
|
||||
<van-icon name="search" size="18"></van-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-table class="gr-table" ref="mesSysTable" :data="data2Show" :border="border" :height="height"
|
||||
:max-height="maxHeight" :stripe="stripe" :size="size" :fit="fit" :show-header="showHeader"
|
||||
:highlight-current-row="highlightCurrentRow" :highlight-selection-row="highlightSelectionRow"
|
||||
:current-row-key="currentRowKey" :row-class-name="rowClassName" :row-style="rowStyle" :cell-class-name="cellClassName"
|
||||
:cell-style="cellStyle" :header-row-class-name="headerRowClassName" :header-row-style="headerRowStyle"
|
||||
:header-cell-class-name="headerCellClassName" :header-cell-style="headerCellStyle" :row-key="rowKey"
|
||||
:empty-text="emptyText" :default-expand-all="defaultExpandAll" :expand-row-keys="expandRowKeys"
|
||||
:default-sort="defaultSort" :tooltip-effect="tooltipEffect" :show-summary="showSummary" :summary-method="summaryMethod"
|
||||
:sum-text="sumText" :span-method="spanMethod" :select-on-indeterminate="selectOnIndeterminate" :indent="indent"
|
||||
:lazy="lazy" :load="load" :tree-props="treeProps"
|
||||
@select="(evt) => $emit('select',evt)" @select-all="(evt) => $emit('select-all',evt)"
|
||||
@selection-change="(evt) => $emit('selection-change',evt)" @cell-mouse-enter="(evt) => $emit('cell-mouse-enter',evt)"
|
||||
@cell-mouse-leave="(evt) => $emit('cell-mouse-leave',evt)" @cell-click="(evt) => $emit('cell-click',evt)"
|
||||
@cell-dblclick="(evt) => $emit('cell-dblclick',evt)" @row-click="(evt) => $emit('row-click',evt)"
|
||||
@row-contextmenu="(evt) => $emit('row-contextmenu',evt)" @row-dblclick="(evt) => $emit('row-dblclick',evt)"
|
||||
@header-click="(evt) => $emit('header-click',evt)" @header-contextmenu="(evt) => $emit('header-contextmenu',evt)"
|
||||
@sort-change="(evt) => $emit('sort-change',evt)" @filter-change="(evt) => $emit('filter-change',evt)"
|
||||
@current-change="(evt) => $emit('current-change',evt)" @header-dragend="(evt) => $emit('header-dragend',evt)"
|
||||
@expand-change="(evt) => $emit('expand-change',evt)" >
|
||||
<el-table-column width="30" type="expand" v-if="meta">
|
||||
<template slot-scope="scope">
|
||||
<el-descriptions :column="1" style="margin: 0 20px;">
|
||||
<el-descriptions-item v-for="(item, index) in meta" :label="item.label" :key="index">{{scope.row[item.prop]}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<slot>
|
||||
</slot>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { Field } from 'vant';
|
||||
Vue.use(Field);
|
||||
import {Icon} from "vant";
|
||||
Vue.use(Icon);
|
||||
export default {
|
||||
props: ["data","meta" ,"major","border","height","maxHeight","stripe","size","fit","showHeader",
|
||||
"highlightCurrentRow","highlightSelectionRow","currentRowKey","rowClassName","rowStyle","cellClassName",
|
||||
"cellStyle","headerRowClassName", "headerRowStyle","headerCellClassName","headerCellStyle",
|
||||
"rowKey", "emptyText","defaultExpandAll", "expandRowKeys","defaultSort","tooltipEffect","sumText","showSummary",
|
||||
"summaryMethod", "spanMethod","selectOnIndeterminate","indent","lazy","load","treeProps",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
data2Show: [],
|
||||
slight: null,
|
||||
mainCol: this.major,
|
||||
rows: null,
|
||||
queryVal: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
this.refreshData();
|
||||
},
|
||||
queryVal(val) {
|
||||
// console.log("queryval ", val);
|
||||
this.refreshData();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDataList(row) {
|
||||
return Object.keys(row)
|
||||
.filter((key) => this.slight.some((s) => s.prop == key)).map((key) => {
|
||||
return {
|
||||
label: this.slight.find((s) => s.prop == key).label,
|
||||
value: row[this.slight.find((s) => s.prop == key).prop],
|
||||
};
|
||||
});
|
||||
},
|
||||
clearSelection(...args) { this.$refs['mesSysTable'].clearSelection(...args)},
|
||||
toggleRowSelection (...args) {this.$refs['mesSysTable'].toggleRowSelection(...args)},
|
||||
toggleAllSelection (...args) { this.$refs['mesSysTable'].toggleAllSelection(...args)},
|
||||
toggleRowExpansion (...args) { this.$refs['mesSysTable'].toggleRowExpansion(...args)},
|
||||
setCurrentRow (...args) { this.$refs['mesSysTable'].setCurrentRow(...args)},
|
||||
clearSort (...args) { this.$refs['mesSysTable'].clearSort(...args)},
|
||||
clearFilter (...args){ this.$refs['mesSysTable'].clearFilter(...args)},
|
||||
doLayout (...args){ this.$refs['mesSysTable'].doLayout(...args)},
|
||||
sort (...args){ this.$refs['mesSysTable'].sort(...args)},
|
||||
refreshData() {
|
||||
if (this.queryVal) {
|
||||
// this.$showLoading();
|
||||
// console.log("queryval is",this.queryVal);
|
||||
this.data2Show = this.data.filter(item => {
|
||||
return JSON.stringify(item).indexOf(this.queryVal) != -1;
|
||||
})
|
||||
// this.$hideLoading();
|
||||
} else {
|
||||
this.data2Show = this.data;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// created() {
|
||||
// this.data2Show = JSON.parse(JSON.stringify(this.data));
|
||||
// },
|
||||
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/styles/vant.scss";
|
||||
|
||||
::v-deep .van-cell {
|
||||
padding: 0px !important
|
||||
}
|
||||
::v-deep .el-table .cell {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.gr-table-container {
|
||||
padding: 0 8px;
|
||||
.gr-table-filter {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 16px;
|
||||
height: 32px;
|
||||
border: 1px solid $gr-border;
|
||||
margin-bottom: 10px;
|
||||
.gr-query-wrap {
|
||||
width: calc(100% - 32px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
.gr-query {
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep th.el-table__cell {
|
||||
background: #0ECD4916 !important;
|
||||
font-family: PingFangSC-Medium;
|
||||
color: #323233;
|
||||
line-height: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
::v-deep .el-table__cell {
|
||||
padding: 8px !important;
|
||||
}
|
||||
::v-deep .el-descriptions__body {
|
||||
font-size: 12px !important;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,94 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table v-if="showTable" ref="mesSysTable" :data="data" :border="border" :height="height"
|
||||
@select="(evt) => $emit('select',evt)" @select-all="(evt) => $emit('select-all',evt)"
|
||||
@selection-change="(evt) => $emit('selection-change',evt)" @cell-mouse-enter="(evt) => $emit('cell-mouse-enter',evt)"
|
||||
>
|
||||
<el-table-column type="expand" v-if="slight && slight[0]">
|
||||
<template slot-scope="scope">
|
||||
<el-descriptions :column="1" style="margin: 0 20px;">
|
||||
<el-descriptions-item v-for="(item, index) in getDataList(scope.row)" :label="item.label" :key="index">{{item.value}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<slot name="default">
|
||||
</slot>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["data", "major","border","height"],
|
||||
data() {
|
||||
return {
|
||||
mainCol: this.major,
|
||||
rows: null,
|
||||
showTable: true,
|
||||
refreshing: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDataList(row) {
|
||||
return Object.keys(row)
|
||||
.filter((key) => this.slight.some((s) => s.prop == key)).map((key) => {
|
||||
return {
|
||||
label: this.slight.find((s) => s.prop == key).label,
|
||||
value: row[this.slight.find((s) => s.prop == key).prop],
|
||||
};
|
||||
});
|
||||
},
|
||||
initData() {
|
||||
let length = this.$slots.default.length, rows = this.$slots.default;
|
||||
// if (window.innerWidth < 768) { //判断PDA
|
||||
if (!this.major || !this.major[0]) { //如果没有指定重要的列,默认第一列(一般是名称)和最后一列(一般是操作)
|
||||
this.mainCol = [rows[0].componentOptions.propsData.prop,rows[length-1].componentOptions.propsData.prop]
|
||||
}
|
||||
this.slight = rows.filter((item) => { //筛选不重要的列,展示到expand中
|
||||
return !(this.mainCol.some((main) => {
|
||||
return main == item.componentOptions.propsData.prop;
|
||||
}));
|
||||
}).map((item) => {
|
||||
return item.componentOptions.propsData;
|
||||
});
|
||||
this.rows = rows.filter((item) => { //去掉不重要的列
|
||||
return this.mainCol.some((main) => {
|
||||
return main == item.componentOptions.propsData.prop;
|
||||
}) || !item.componentOptions.propsData.prop;
|
||||
});
|
||||
this.$slots.default = this.rows;
|
||||
// }
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initData();
|
||||
},
|
||||
updated() {
|
||||
if (!this.refreshing) {
|
||||
this.refreshing = true;
|
||||
this.showTable = false;
|
||||
} else{
|
||||
setTimeout(()=> {
|
||||
this.showTable = true;
|
||||
this.initData();
|
||||
},0)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
::v-deep th.el-table__cell {
|
||||
background: #0ECD4916 !important;
|
||||
font-family: PingFangSC-Medium;
|
||||
// font-size: 16px;
|
||||
color: #323233;
|
||||
line-height: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
::v-deep .el-table__cell {
|
||||
padding: 8px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,117 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table ref="mesSysTable" :data="data" :border="border" :height="height"
|
||||
:max-height="maxHeight" :stripe="stripe" :size="size" :fit="fit" :show-header="showHeader"
|
||||
:highlight-current-row="highlightCurrentRow" :highlight-selection-row="highlightSelectionRow"
|
||||
:current-row-key="currentRowKey" :row-class-name="rowClassName" :row-style="rowStyle" :cell-class-name="cellClassName"
|
||||
:cell-style="cellStyle" :header-row-class-name="headerRowClassName" :header-row-style="headerRowStyle"
|
||||
:header-cell-class-name="headerCellClassName" :header-cell-style="headerCellStyle" :row-key="rowKey"
|
||||
:empty-text="emptyText" :default-expand-all="defaultExpandAll" :expand-row-keys="expandRowKeys"
|
||||
:default-sort="defaultSort" :tooltip-effect="tooltipEffect" :show-summary="showSummary" :summary-method="summaryMethod"
|
||||
:sum-text="sumText" :span-method="spanMethod" :select-on-indeterminate="selectOnIndeterminate" :indent="indent"
|
||||
:lazy="lazy" :load="load" :tree-props="treeProps"
|
||||
@select="(evt) => $emit('select',evt)" @select-all="(evt) => $emit('select-all',evt)"
|
||||
@selection-change="(evt) => $emit('selection-change',evt)" @cell-mouse-enter="(evt) => $emit('cell-mouse-enter',evt)"
|
||||
@cell-mouse-leave="(evt) => $emit('cell-mouse-leave',evt)" @cell-click="(evt) => $emit('cell-click',evt)"
|
||||
@cell-dblclick="(evt) => $emit('cell-dblclick',evt)" @row-click="(evt) => $emit('row-click',evt)"
|
||||
@row-contextmenu="(evt) => $emit('row-contextmenu',evt)" @row-dblclick="(evt) => $emit('row-dblclick',evt)"
|
||||
@header-click="(evt) => $emit('header-click',evt)" @header-contextmenu="(evt) => $emit('header-contextmenu',evt)"
|
||||
@sort-change="(evt) => $emit('sort-change',evt)" @filter-change="(evt) => $emit('filter-change',evt)"
|
||||
@current-change="(evt) => $emit('current-change',evt)" @header-dragend="(evt) => $emit('header-dragend',evt)"
|
||||
@expand-change="(evt) => $emit('expand-change',evt)" >
|
||||
<el-table-column type="expand" v-if="slight && slight[0]">
|
||||
<template slot-scope="scope">
|
||||
<el-descriptions :column="1" style="margin: 0 20px;">
|
||||
<el-descriptions-item v-for="(item, index) in getDataList(scope.row)" :label="item.label" :key="index">{{item.value}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<slot>
|
||||
</slot>
|
||||
</el-table>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["data", "major","border","height","maxHeight","stripe","size","fit","showHeader",
|
||||
"highlightCurrentRow","highlightSelectionRow","currentRowKey","rowClassName","rowStyle","cellClassName",
|
||||
"cellStyle","headerRowClassName", "headerRowStyle","headerCellClassName","headerCellStyle",
|
||||
"rowKey", "emptyText","defaultExpandAll", "expandRowKeys","defaultSort","tooltipEffect","sumText","showSummary",
|
||||
"summaryMethod", "spanMethod","selectOnIndeterminate","indent","lazy","load","treeProps",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
slight: null,
|
||||
mainCol: this.major,
|
||||
rows: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getDataList(row) {
|
||||
return Object.keys(row)
|
||||
.filter((key) => this.slight.some((s) => s.prop == key)).map((key) => {
|
||||
return {
|
||||
label: this.slight.find((s) => s.prop == key).label,
|
||||
value: row[this.slight.find((s) => s.prop == key).prop],
|
||||
};
|
||||
});
|
||||
},
|
||||
clearSelection(...args) { this.$refs['mesSysTable'].clearSelection(...args)},
|
||||
toggleRowSelection (...args) {this.$refs['mesSysTable'].toggleRowSelection(...args)},
|
||||
toggleAllSelection (...args) { this.$refs['mesSysTable'].toggleAllSelection(...args)},
|
||||
toggleRowExpansion (...args) { this.$refs['mesSysTable'].toggleRowExpansion(...args)},
|
||||
setCurrentRow (...args) { this.$refs['mesSysTable'].setCurrentRow(...args)},
|
||||
clearSort (...args) { this.$refs['mesSysTable'].clearSort(...args)},
|
||||
clearFilter (...args){ this.$refs['mesSysTable'].clearFilter(...args)},
|
||||
doLayout (...args){ this.$refs['mesSysTable'].doLayout(...args)},
|
||||
sort (...args){ this.$refs['mesSysTable'].sort(...args)},
|
||||
|
||||
initData() {
|
||||
let length = this.$slots.default.length, rows = this.$slots.default;
|
||||
// if (window.innerWidth < 768) { //判断PDA
|
||||
if (!this.major || !this.major[0]) { //如果没有指定重要的列,默认第一列(一般是名称)和最后一列(一般是操作)
|
||||
this.mainCol = [rows[0].componentOptions.propsData.prop,rows[length-1].componentOptions.propsData.prop]
|
||||
}
|
||||
this.slight = rows.filter((item) => { //筛选不重要的列,展示到expand中
|
||||
return !(this.mainCol.some((main) => {
|
||||
return main == item.componentOptions.propsData.prop;
|
||||
}));
|
||||
}).map((item) => {
|
||||
return item.componentOptions.propsData;
|
||||
});
|
||||
this.rows = rows.filter((item) => { //去掉不重要的列
|
||||
return this.mainCol.some((main) => {
|
||||
return main == item.componentOptions.propsData.prop;
|
||||
}) || !item.componentOptions.propsData.prop;
|
||||
});
|
||||
this.$slots.default = this.rows;
|
||||
// }
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initData();
|
||||
},
|
||||
updated() {
|
||||
console.log("updated!!!!");
|
||||
// this.initData();
|
||||
// this.$slots.default = this.rows;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
::v-deep th.el-table__cell {
|
||||
background: #0ECD4916 !important;
|
||||
font-family: PingFangSC-Medium;
|
||||
// font-size: 16px;
|
||||
color: #323233;
|
||||
line-height: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
::v-deep .el-table__cell {
|
||||
padding: 8px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
供应商系统需要增加一个接口
|
||||
请求参数:最后更新时间,支持分页。
|
||||
返回:
|
||||
1、供应商账号注册信息(企业类型 企业名称 邮箱 账号 密码 公司地址 税号 联系人 联系电话) 启用 删除 禁用 标识 根据修改时间倒序排序返回。
|
||||
2、整个信息可以使用AES加密,提供解密算法,密码不可逆,提供提供算法进行比对。(因为数据返回都是加密的,且加密算法只提供给你们 不要到时候说我们接口信息安全泄露哈)
|
||||
|
||||
|
||||
https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts?lastUpdatedTime=2026-08-01%2000:00:00&pageNum=1&pageSize=20
|
||||
|
||||
测试环境的密钥配置如下
|
||||
supplier-account-api-aes:
|
||||
key-id: supplier-api-key
|
||||
key: 'kY0+oIO/laeaaaYDd+9TCYbCQR0b/vqMeJ9sUnSVd9U='
|
||||
@@ -59,6 +59,18 @@ ruoyi:
|
||||
camera:
|
||||
base-url: https://risingdoctor.com/camera/
|
||||
|
||||
# 供应商账号接口 (拉取/解密) 配置
|
||||
supplier-account-api:
|
||||
base-url: https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts
|
||||
# 每页条数
|
||||
page-size: 20
|
||||
# 每次拉取最近 N 分钟更新的数据
|
||||
pull-minutes: 5
|
||||
# 供应商账号接口 AES 密钥 (数据解密用, 测试环境密钥)
|
||||
supplier-account-api-aes:
|
||||
key-id: supplier-api-key
|
||||
key: 'kY0+oIO/laeaaaYDd+9TCYbCQR0b/vqMeJ9sUnSVd9U='
|
||||
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为8080
|
||||
|
||||
+11
@@ -48,6 +48,17 @@ public class BizExpertController extends BaseController
|
||||
{
|
||||
return success(bizExpertService.getByUserId(SecurityUtils.getUserId()));
|
||||
}
|
||||
/**
|
||||
* 按手机号查专家 (参会人 dialog 手机号放大镜回填用): 命中返回专家档案, 未命中返回 null
|
||||
*/
|
||||
@GetMapping("/byPhone/{phone}")
|
||||
public AjaxResult getByPhone(@PathVariable String phone)
|
||||
{
|
||||
BizExpert q = new BizExpert();
|
||||
q.setPhone(phone);
|
||||
List<BizExpert> list = bizExpertService.selectList(q);
|
||||
return success(list != null && !list.isEmpty() ? list.get(0) : null);
|
||||
}
|
||||
/**
|
||||
* admin 创建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号)
|
||||
* 返回 SysUser (含明文 password 给前端 toast 用)
|
||||
|
||||
+20
-4
@@ -259,6 +259,9 @@ public class BizProjectController extends BaseController
|
||||
@PostMapping("/{projectId}/assigns")
|
||||
public AjaxResult saveAssigns(@PathVariable("projectId") Long projectId, @RequestBody List<BizProjectAssign> assigns)
|
||||
{
|
||||
// 已结题项目不能再分配
|
||||
BizProject project = requireAssignableProject(projectId);
|
||||
|
||||
if (assigns == null) assigns = new ArrayList<>();
|
||||
bizProjectAssignService.validateSum(projectId, assigns);
|
||||
// #3 通知去重: 拉旧数据按 executionUnitId 索引, 同 (executionUnitId, sessions, amount) → 无变化 → 跳过
|
||||
@@ -270,8 +273,7 @@ public class BizProjectController extends BaseController
|
||||
}
|
||||
bizProjectAssignService.deleteByProjectId(projectId);
|
||||
// #3 通知: 查一次项目名, 避免循环里重复查 DB
|
||||
BizProject project = bizProjectService.getById(projectId);
|
||||
String projectName = project != null ? project.getProjectName() : null;
|
||||
String projectName = project.getProjectName();
|
||||
for (BizProjectAssign a : assigns) {
|
||||
a.setProjectId(projectId);
|
||||
if (a.getStatus() == null) a.setStatus("0");
|
||||
@@ -331,6 +333,15 @@ public class BizProjectController extends BaseController
|
||||
try { return Long.parseLong(s); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
|
||||
/** 校验项目存在且未结题 — 已结题项目禁止一切分配操作, 返回项目供取项目名. */
|
||||
private BizProject requireAssignableProject(Long projectId) {
|
||||
if (projectId == null) throw new ServiceException("项目不存在");
|
||||
BizProject p = bizProjectService.getById(projectId);
|
||||
if (p == null) throw new ServiceException("项目不存在");
|
||||
if ("1".equals(p.getIsFinished())) throw new ServiceException("已结题项目不能分配");
|
||||
return p;
|
||||
}
|
||||
|
||||
@Log(title = "项目执行方分配", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{projectId}/assigns")
|
||||
public AjaxResult clearAssigns(@PathVariable("projectId") Long projectId)
|
||||
@@ -406,6 +417,8 @@ public class BizProjectController extends BaseController
|
||||
if (body.getProjectId() == null) {
|
||||
return error("projectId 必填");
|
||||
}
|
||||
// 已结题项目不能分配
|
||||
BizProject project = requireAssignableProject(parseProjectId(body.getProjectId()));
|
||||
java.util.List<Long> mids = body.getMonitorUserIds();
|
||||
if (mids == null || mids.isEmpty()) {
|
||||
// 向后兼容: 单值 monitorUserId
|
||||
@@ -430,8 +443,7 @@ public class BizProjectController extends BaseController
|
||||
|
||||
// 通知被分配的监察员 (新增 / 说明或积分变化才发). projectId 在 sponsor_assign 是 String, 转 Long 查主表
|
||||
Long projectIdLong = parseProjectId(body.getProjectId());
|
||||
BizProject project = projectIdLong != null ? bizProjectService.getById(projectIdLong) : null;
|
||||
String projectName = project != null ? project.getProjectName() : null;
|
||||
String projectName = project.getProjectName();
|
||||
for (Long mid : mids) {
|
||||
if (mid == null) continue;
|
||||
BizProjectSponsorAssign old = oldByMonitor.get(mid);
|
||||
@@ -467,6 +479,8 @@ public class BizProjectController extends BaseController
|
||||
if (body.getProjectId() == null) {
|
||||
return error("projectId 必填");
|
||||
}
|
||||
// 已结题项目不能分配
|
||||
requireAssignableProject(parseProjectId(body.getProjectId()));
|
||||
java.util.List<Long> sids = body.getStaffUserIds();
|
||||
if (sids == null || sids.isEmpty()) {
|
||||
// 向后兼容: 单值 staffUserId
|
||||
@@ -532,6 +546,8 @@ public class BizProjectController extends BaseController
|
||||
if (body.getProjectId() == null || body.getMonitorUserId() == null) {
|
||||
throw new IllegalArgumentException("projectId / monitorUserId 必填");
|
||||
}
|
||||
// 已结题项目不能分配 (抛 ServiceException → 本循环 catch 计入 errors)
|
||||
requireAssignableProject(parseProjectId(body.getProjectId()));
|
||||
body.setCreateBy(loginName);
|
||||
body.setSponsorOrgId(bizOrgService.selectOrgIdByUserId(loginUid));
|
||||
// 通知去重: 拉旧分配, 找同 monitorUserId, 比较 assignDesc/assignPoints 是否变化
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package com.ruoyi.business.scheduler;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.business.supplier.SupplierAccountApiCodec;
|
||||
import com.ruoyi.common.utils.http.HttpUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 供应商账号数据拉取调度器: 每分钟拉取最近 5 分钟更新的账号, 解密后打印.
|
||||
* <p>
|
||||
* 数据源: {@code GET /supplier-api/bidding/supplier/openapi/accounts}
|
||||
* 入参 lastUpdatedTime(最后更新时间) / pageNum / pageSize, 按更新时间倒序返回.
|
||||
* 返回 data 字段为 AES-256-GCM 加密串, 用 {@link SupplierAccountApiCodec} 解密.
|
||||
* <p>
|
||||
* 说明: 只打印不落库 (后续需要持久化时再扩展).
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SupplierAccountPullScheduler
|
||||
{
|
||||
private static final String KEY = "supplier-account-api-aes.key";
|
||||
private static final String BASE_URL = "supplier-account-api.base-url";
|
||||
private static final String PAGE_SIZE = "supplier-account-api.page-size";
|
||||
private static final String PULL_MINUTES = "supplier-account-api.pull-minutes";
|
||||
|
||||
private static final String DEFAULT_BASE_URL =
|
||||
"https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts";
|
||||
|
||||
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||
public void pullAccounts()
|
||||
{
|
||||
try
|
||||
{
|
||||
String key = env.getProperty(KEY);
|
||||
if (key == null || key.isEmpty())
|
||||
{
|
||||
log.warn("[SupplierAccountPull] 未配置 {} , 跳过", KEY);
|
||||
return;
|
||||
}
|
||||
String baseUrl = env.getProperty(BASE_URL, DEFAULT_BASE_URL);
|
||||
int pageSize = env.getProperty(PAGE_SIZE, Integer.class, 20);
|
||||
int pullMinutes = env.getProperty(PULL_MINUTES, Integer.class, 5*24*60*60);
|
||||
String lastUpdatedTime = LocalDateTime.now().minusMinutes(pullMinutes).format(TIME_FMT);
|
||||
|
||||
int pageNum = 1;
|
||||
int fetched = 0;
|
||||
while (true)
|
||||
{
|
||||
String param = "lastUpdatedTime=" + URLEncoder.encode(lastUpdatedTime, "UTF-8")
|
||||
+ "&pageNum=" + pageNum + "&pageSize=" + pageSize;
|
||||
String resp = HttpUtils.sendGet(baseUrl, param);
|
||||
if (resp == null || resp.isEmpty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
JsonNode root = objectMapper.readTree(resp);
|
||||
String encrypted = root.path("data").asText("");
|
||||
if (encrypted.isEmpty())
|
||||
{
|
||||
log.info("[SupplierAccountPull] page={} 无 data 字段 (code={}, msg={}), 结束",
|
||||
pageNum, root.path("code").asText(), root.path("msg").asText());
|
||||
break;
|
||||
}
|
||||
|
||||
String plain = SupplierAccountApiCodec.decrypt(encrypted, key);
|
||||
JsonNode inner = objectMapper.readTree(plain);
|
||||
int total = inner.path("total").asInt(0);
|
||||
JsonNode rows = inner.path("rows");
|
||||
int size = rows.isArray() ? rows.size() : 0;
|
||||
fetched += size;
|
||||
|
||||
// 只打印即可: 整页明文 JSON 打出来 (供观察/后续落库)
|
||||
log.info("[SupplierAccountPull] page={} total={} 本页={} 明文: {}", pageNum, total, size, plain);
|
||||
|
||||
if (size == 0 || fetched >= total)
|
||||
{
|
||||
break;
|
||||
}
|
||||
pageNum++;
|
||||
}
|
||||
log.info("[SupplierAccountPull] 本轮完成, 共 {} 条", fetched);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("[SupplierAccountPull] 拉取失败 (跳过, 下分钟再试)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -3,7 +3,9 @@ package com.ruoyi.business.service.impl;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
@@ -67,7 +69,19 @@ public class BizMeetingServiceImpl implements IBizMeetingService
|
||||
if (entity.getServiceAuditStage() == null || entity.getServiceAuditStage().isEmpty()) {
|
||||
entity.setServiceAuditStage("NOT_SUBMITTED");
|
||||
}
|
||||
return bizMeetingMapper.insert(entity);
|
||||
try {
|
||||
return bizMeetingMapper.insert(entity);
|
||||
}
|
||||
catch (DuplicateKeyException e) {
|
||||
// 入库时 meeting_id 撞库 (Redis 序列被重置/回退): 自动加 10 + 随机(0~10) 重新取一次, 不直接报错
|
||||
Long oldId = entity.getMeetingId();
|
||||
if (oldId == null) {
|
||||
throw e;
|
||||
}
|
||||
long retryId = oldId + 10L + ThreadLocalRandom.current().nextInt(11);
|
||||
entity.setMeetingId(retryId);
|
||||
return bizMeetingMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-4
@@ -79,9 +79,14 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
}
|
||||
|
||||
// 1. 创建 sys_user 子账号
|
||||
// 继承主账号 role_type, 避免子账号被 sys_user.role_type DEFAULT 'executor' 覆盖
|
||||
// (之前不写 roleType 时, sponsor 主账号的子账号会被 DEFAULT 错位成 executor)
|
||||
// role_type 取 person.unitType (sponsor/executor/doctor), 而非继承创建者角色:
|
||||
// 否则 admin/manager 在 admin/sponsor-people 建人会把子账号错位成 admin/manager (后台管理员).
|
||||
// 仅当 unitType 缺失时才回退到主账号 role_type 兜底.
|
||||
SysUser mainUser = mainUserId == null ? null : sysUserMapper.selectUserById(mainUserId);
|
||||
String roleType = entity.getUnitType();
|
||||
if (roleType == null || roleType.isEmpty()) {
|
||||
roleType = mainUser != null ? mainUser.getRoleType() : null;
|
||||
}
|
||||
SysUser newUser = new SysUser();
|
||||
newUser.setUserName(entity.getLoginUsername());
|
||||
newUser.setNickName(entity.getName());
|
||||
@@ -92,8 +97,8 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
newUser.setParentUserId(mainUserId);
|
||||
newUser.setStatus("0");
|
||||
newUser.setDelFlag("0");
|
||||
if (mainUser != null && mainUser.getRoleType() != null) {
|
||||
newUser.setRoleType(mainUser.getRoleType());
|
||||
if (roleType != null) {
|
||||
newUser.setRoleType(roleType);
|
||||
}
|
||||
newUser.setCreateBy(SecurityUtils.getUsername());
|
||||
// mybatis useGeneratedKeys 自动回填 userId
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.ruoyi.business.supplier;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* 供应商账号接口 AES 解密工具 (对应 refer/BiddingSupplierAccountApiCodec).
|
||||
* <p>
|
||||
* 接口返回的 {@code data} 字段格式: {@code BSA.v1.<keyId>.<nonce>.<ciphertext>}
|
||||
* <ul>
|
||||
* <li>BSA — 固定前缀 (Bidding Supplier Account)</li>
|
||||
* <li>v1 — 版本号</li>
|
||||
* <li>keyId — 密钥标识, 对应配置 {@code supplier-account-api-aes.key-id}
|
||||
* (单密钥场景忽略, 密钥旋转时按此取对应 key)</li>
|
||||
* <li>nonce — 12 字节随机 IV, URL-safe Base64 编码 (无填充)</li>
|
||||
* <li>ciphertext — 密文, URL-safe Base64 编码 (无填充, 末尾带 16 字节 GCM 认证标签)</li>
|
||||
* </ul>
|
||||
* 算法: AES-256/GCM/NoPadding (密钥 32 字节, GCM 认证标签 128 bit).
|
||||
*/
|
||||
public class SupplierAccountApiCodec
|
||||
{
|
||||
private static final int GCM_TAG_BITS = 128;
|
||||
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
|
||||
private SupplierAccountApiCodec() {}
|
||||
|
||||
/**
|
||||
* 解密接口返回的 data 字段, 得到明文 JSON 字符串.
|
||||
*
|
||||
* @param data 接口返回的 data 字段 ({@code BSA.v1.keyId.nonce.ciphertext})
|
||||
* @param key AES 密钥 (Base64 编码, 解码后 32 字节)
|
||||
*/
|
||||
public static String decrypt(String data, String key)
|
||||
{
|
||||
if (data == null || data.isEmpty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String[] parts = data.split("\\.");
|
||||
if (parts.length < 5)
|
||||
{
|
||||
throw new IllegalArgumentException("供应商接口 data 格式非法 (期望 BSA.v1.keyId.nonce.ciphertext)");
|
||||
}
|
||||
byte[] keyBytes = Base64.getDecoder().decode(key);
|
||||
byte[] nonce = decodeBase64Url(parts[3]);
|
||||
byte[] ciphertext = decodeBase64Url(parts[4]);
|
||||
try
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, "AES"),
|
||||
new GCMParameterSpec(GCM_TAG_BITS, nonce));
|
||||
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new IllegalStateException("供应商接口数据解密失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/** URL-safe Base64 → bytes (兼容 '-'/'_' 且无填充) */
|
||||
private static byte[] decodeBase64Url(String s)
|
||||
{
|
||||
String b = s.replace('-', '+').replace('_', '/');
|
||||
while (b.length() % 4 != 0)
|
||||
{
|
||||
b += "=";
|
||||
}
|
||||
return Base64.getDecoder().decode(b);
|
||||
}
|
||||
}
|
||||
@@ -49,15 +49,17 @@
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
p.is_deleted = 0
|
||||
<if test="planName != null and planName != ''"> and p.plan_name like concat('%', #{planName}, '%')</if>
|
||||
<if test="planDirectionId != null"> and p.plan_direction_id = #{planDirectionId}</if>
|
||||
<if test="planCategory != null and planCategory != ''"> and p.plan_category = #{planCategory}</if>
|
||||
<if test="projectForm != null and projectForm != ''"> and p.project_form = #{projectForm}</if>
|
||||
<if test="submitterId != null"> and p.submitter_id = #{submitterId}</if>
|
||||
<choose>
|
||||
<!-- 选了具体状态: 等值匹配 -->
|
||||
<when test="status != null and status != ''"> and p.status = #{status}</when>
|
||||
<!-- 未选: 不加 status 过滤, 由前端按角色决定默认值 (经理侧默认查 1/2/3, 医生侧默认查全部含 0) -->
|
||||
</choose>
|
||||
<if test="remark != null and remark != ''"> and p.remark = #{remark}</if>
|
||||
<if test="remark != null and remark != ''"> and p.remark like concat('%', #{remark}, '%')</if>
|
||||
</where>
|
||||
order by p.plan_id desc
|
||||
</select>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# 开发环境 API 基路径: 直连远程后端 (绕过 Vite proxy, 避免 HTTPS 代理卡住)
|
||||
VITE_APP_BASE_API = 'https://risingdoctor.com/hg-api'
|
||||
# 开发环境 API 基路径: 本地后端 (走 Vite proxy 转发到 localhost:8080)
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
@@ -10,8 +10,12 @@
|
||||
/>
|
||||
-->
|
||||
<template>
|
||||
<div class="ht-file-upload" :class="{ 'has-file': modelValue, 'is-block': block, readonly }" @click="handleClick">
|
||||
<div v-if="!modelValue && !readonly" class="ht-file-placeholder">
|
||||
<div class="ht-file-upload" :class="{ 'has-file': modelValue, 'is-block': block, readonly, uploading }" @click="handleClick">
|
||||
<div v-if="uploading" class="ht-file-progress">
|
||||
<span class="progress-label">上传中</span>
|
||||
<el-progress :percentage="progress" :stroke-width="6" class="progress-bar" />
|
||||
</div>
|
||||
<div v-else-if="!modelValue && !readonly" class="ht-file-placeholder">
|
||||
<span class="placeholder-text">{{ placeholder }}</span>
|
||||
<span class="placeholder-hint" v-if="hint">{{ hint }}</span>
|
||||
</div>
|
||||
@@ -69,6 +73,7 @@ const emit = defineEmits(['update:modelValue', 'update:name'])
|
||||
|
||||
const fileInput = ref(null)
|
||||
const uploading = ref(false)
|
||||
const progress = ref(0)
|
||||
|
||||
const fileName = computed(() => {
|
||||
// 优先用调用方回传的原文件名 (v-model:name); 没有则从 OSS key 还原原名
|
||||
@@ -132,8 +137,9 @@ async function onFileChange(e) {
|
||||
return ElMessage.warning(`文件大小不能超过 ${props.maxSize}MB`)
|
||||
}
|
||||
uploading.value = true
|
||||
progress.value = 0
|
||||
try {
|
||||
const url = await uploadToOss(file, props.dir)
|
||||
const url = await uploadToOss(file, props.dir, (p) => { progress.value = p })
|
||||
emit('update:modelValue', url)
|
||||
emit('update:name', file.name)
|
||||
ElMessage.success('上传成功')
|
||||
@@ -167,6 +173,18 @@ function onRemove() {
|
||||
.ht-file-upload.has-file { border-style: solid; border-color: #52c41a; background: #fff; }
|
||||
.ht-file-upload.is-block { width: 100%; }
|
||||
.ht-file-upload.readonly { cursor: default; }
|
||||
.ht-file-upload.uploading { cursor: default; }
|
||||
|
||||
/* 上传进度条 */
|
||||
.ht-file-progress {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.progress-label { font-size: 13px; color: #606266; white-space: nowrap; flex-shrink: 0; }
|
||||
.progress-bar { flex: 1; }
|
||||
|
||||
.ht-file-placeholder {
|
||||
flex: 1;
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<!-- 公开门户共享顶部导航 (首页 / 项目公示 / 公示详情 共用)
|
||||
active 态按当前 route 推导, 无需各页面各自传参 -->
|
||||
<header class="top-nav" :class="topNavClass">
|
||||
<a class="logo" title="返回首页" @click.prevent="goHome">
|
||||
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-title">北京整合医学学会</span>
|
||||
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item"><a class="nav-link" :class="{ active: isHome }" @click.prevent="goHome">年度项目规划</a></li>
|
||||
<li class="nav-item"><a class="nav-link" :class="{ active: isPublicity }" @click.prevent="goPublicity">项目公示</a></li>
|
||||
</ul>
|
||||
<div class="top-tools">
|
||||
<template v-if="!loggedIn">
|
||||
<span class="login-btn" @click="goLogin">登录</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-dropdown trigger="click" @command="onUserCmd">
|
||||
<a class="user-link" @click.prevent>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<span>{{ userName }}</span>
|
||||
</a>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="account">我的主页</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 手机端汉堡按钮 (桌面隐藏) -->
|
||||
<button class="hamburger" type="button" aria-label="打开菜单" @click="drawerOpen = true">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- 手机端抽屉菜单 (桌面隐藏) -->
|
||||
<transition name="drawer-fade">
|
||||
<div v-if="drawerOpen" class="drawer-mask" @click="drawerOpen = false"></div>
|
||||
</transition>
|
||||
<transition name="drawer-slide">
|
||||
<aside v-if="drawerOpen" class="drawer-panel" role="dialog" aria-label="导航菜单">
|
||||
<div class="drawer-header">
|
||||
<span class="drawer-title">导航菜单</span>
|
||||
<button class="drawer-close" type="button" aria-label="关闭菜单" @click="drawerOpen = false">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
<line x1="6" y1="18" x2="18" y2="6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="drawer-nav">
|
||||
<a class="drawer-link" @click.prevent="goHomeAndClose">年度项目规划</a>
|
||||
<a class="drawer-link" @click.prevent="goPublicityAndClose">项目公示</a>
|
||||
</nav>
|
||||
<div class="drawer-divider"></div>
|
||||
<div class="drawer-tools">
|
||||
<template v-if="!loggedIn">
|
||||
<span class="drawer-login" @click="goLoginAndClose">登录</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="drawer-user">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<span>{{ userName }}</span>
|
||||
</div>
|
||||
<a class="drawer-link" @click.prevent="goAccountAndClose">我的主页</a>
|
||||
<a class="drawer-link danger" @click.prevent="goLogoutAndClose">退出登录</a>
|
||||
</template>
|
||||
</div>
|
||||
</aside>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isScrolled = ref(false)
|
||||
const drawerOpen = ref(false)
|
||||
|
||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||
const loggedIn = computed(() => !!userStore.token)
|
||||
const userName = computed(() => userStore.user?.userName || '用户')
|
||||
|
||||
// active 态: 按当前路由推导 (首页=年度项目规划, /publicity* = 项目公示)
|
||||
const isHome = computed(() => route.path === '/')
|
||||
const isPublicity = computed(() => route.path === '/publicity' || route.path.startsWith('/publicity/'))
|
||||
|
||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||
onMounted(() => window.addEventListener('scroll', handleScroll, { passive: true }))
|
||||
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
||||
|
||||
// 当前已在目标页时点导航 = 仅回顶, 否则跳转
|
||||
function goHome() { if (route.path !== '/') router.push('/'); else window.scrollTo({ top: 0, behavior: 'smooth' }) }
|
||||
function goPublicity() { if (route.path !== '/publicity') router.push('/publicity'); else window.scrollTo({ top: 0, behavior: 'smooth' }) }
|
||||
function goLogin() { router.push('/login') }
|
||||
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
|
||||
|
||||
async function onUserCmd(cmd) {
|
||||
if (cmd === 'logout') return goLogout()
|
||||
if (cmd === 'account') {
|
||||
const r = (userStore.user?.role || '')
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||
router.push(map[r] || '/admin/workbench')
|
||||
}
|
||||
}
|
||||
|
||||
// 抽屉版导航: 跳转后关闭抽屉
|
||||
function goHomeAndClose() { drawerOpen.value = false; goHome() }
|
||||
function goPublicityAndClose() { drawerOpen.value = false; goPublicity() }
|
||||
function goLoginAndClose() { drawerOpen.value = false; goLogin() }
|
||||
function goAccountAndClose() { drawerOpen.value = false; onUserCmd('account') }
|
||||
async function goLogoutAndClose() { drawerOpen.value = false; await goLogout() }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* ========== 顶部导航 ========== */
|
||||
.top-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
height: 72px;
|
||||
padding: 0 60px;
|
||||
background: var(--brand-primary);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.top-nav.is-scrolled {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logo-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
|
||||
|
||||
.logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 2px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 36px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
cursor: pointer;
|
||||
transition: color 0.25s;
|
||||
position: relative;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
transform: translateX(-50%);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-link,
|
||||
.nav-link.active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-link::after,
|
||||
.nav-link.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 顶部工具栏 */
|
||||
.top-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
padding: 7px 20px;
|
||||
background: #fff;
|
||||
color: var(--brand-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.user-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.user-link:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ========== 汉堡按钮 (桌面隐藏, 手机显示) ========== */
|
||||
.hamburger {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.hamburger:hover { background: rgba(255, 255, 255, 0.12); }
|
||||
.hamburger:active { background: rgba(255, 255, 255, 0.2); }
|
||||
|
||||
/* ========== 抽屉 (手机端) ========== */
|
||||
.drawer-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 1999;
|
||||
}
|
||||
.drawer-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 260px;
|
||||
max-width: 80vw;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
z-index: 2000;
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 16px 0 20px;
|
||||
border-bottom: 1px solid var(--brand-slate-200);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.drawer-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.drawer-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
.drawer-close:hover { background: var(--brand-slate-100); color: var(--el-text-color-primary); }
|
||||
|
||||
.drawer-nav { display: flex; flex-direction: column; padding: 8px 0; }
|
||||
.drawer-link {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.drawer-link:hover { background: var(--brand-slate-50); }
|
||||
.drawer-link.danger { color: var(--el-color-danger); }
|
||||
|
||||
.drawer-divider { height: 1px; background: var(--brand-slate-200); margin: 4px 20px; }
|
||||
|
||||
.drawer-tools { display: flex; flex-direction: column; padding: 8px 0; gap: 0; }
|
||||
.drawer-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
border-bottom: 1px solid var(--brand-slate-100);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.drawer-user svg { color: var(--brand-primary); }
|
||||
.drawer-login {
|
||||
display: block;
|
||||
margin: 4px 20px;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
background: var(--brand-primary);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.drawer-login:hover { opacity: 0.9; }
|
||||
|
||||
/* 抽屉过渡 */
|
||||
.drawer-fade-enter-active, .drawer-fade-leave-active { transition: opacity 0.2s; }
|
||||
.drawer-fade-enter-from, .drawer-fade-leave-to { opacity: 0; }
|
||||
.drawer-slide-enter-active, .drawer-slide-leave-active { transition: transform 0.25s ease; }
|
||||
.drawer-slide-enter-from, .drawer-slide-leave-to { transform: translateX(100%); }
|
||||
|
||||
/* ========== 移动端适配 ========== */
|
||||
@media (max-width: 768px) {
|
||||
/* 顶栏: 高度收窄 + padding 减小 */
|
||||
.top-nav { height: 56px !important; padding: 0 16px !important; }
|
||||
.logo-title { font-size: 14px !important; }
|
||||
.logo-subtitle { display: none; }
|
||||
/* 桌面 nav-list + 顶部 tools 隐藏, 改用汉堡 */
|
||||
.nav-list { display: none !important; }
|
||||
.top-tools { display: none !important; }
|
||||
.hamburger { display: inline-flex !important; }
|
||||
}
|
||||
</style>
|
||||
+21
-10
@@ -43,7 +43,7 @@ function getMimeByExt(name) {
|
||||
return map[ext] || 'application/octet-stream'
|
||||
}
|
||||
|
||||
export async function uploadToOss(file, dir) {
|
||||
export async function uploadToOss(file, dir, onProgress) {
|
||||
const sign = await getOssSign(dir)
|
||||
// 原文件名进 OSS key: 原始名称_时间戳_随机串.原扩展名 (保留中文, 仅清洗 URL/路径危险字符)
|
||||
// 这样上传控件能从 URL 里还原原名显示, 无需额外 name 字段/列
|
||||
@@ -67,14 +67,25 @@ export async function uploadToOss(file, dir) {
|
||||
|
||||
// ⚠️ 不要手动设置 Content-Type header,会让 multipart/form-data boundary 丢失
|
||||
// FormData 会自动生成正确的 multipart 格式
|
||||
const resp = await fetch(sign.host, {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
// 用 XMLHttpRequest 代替 fetch: fetch 拿不到上传进度, XHR 的 upload.onprogress 可回调进度
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', sign.host)
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable && onProgress) {
|
||||
onProgress(Math.min(100, Math.round((e.loaded / e.total) * 100)))
|
||||
}
|
||||
}
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
// 返回 URL 时对路径逐段编码 (保留 / 分隔), 让含中文/特殊字符的 key 在所有下载场景下都是 ASCII 安全 URL
|
||||
// OSS 对象 key 本身仍是原始值 (FormData 的 key 字段未编码), 下载时 OSS 会自动把 %XX 还原
|
||||
resolve(sign.host + '/' + key.split('/').map(encodeURIComponent).join('/'))
|
||||
} else {
|
||||
reject(new Error('OSS 上传失败: HTTP ' + xhr.status))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => reject(new Error('OSS 上传失败: 网络错误'))
|
||||
xhr.send(fd)
|
||||
})
|
||||
if (!resp.ok) {
|
||||
throw new Error('OSS 上传失败: HTTP ' + resp.status)
|
||||
}
|
||||
// 返回 URL 时对路径逐段编码 (保留 / 分隔), 让含中文/特殊字符的 key 在所有下载场景下都是 ASCII 安全 URL
|
||||
// OSS 对象 key 本身仍是原始值 (FormData 的 key 字段未编码), 下载时 OSS 会自动把 %XX 还原
|
||||
return sign.host + '/' + key.split('/').map(encodeURIComponent).join('/')
|
||||
}
|
||||
@@ -358,6 +358,8 @@ const roleHome = {
|
||||
/** redirect 是否属于当前角色: 防止 doctor 拿到 manager 的 redirect 后跳过去被踢回 login ("现在评审专家登录后不跳转" 的根本原因) */
|
||||
function redirectBelongsToRole(path, role) {
|
||||
if (!path) return false
|
||||
// 公开门户详情页 (专项计划详情) 允许回跳: 未登录点击计划详情 → 登录后回详情
|
||||
if (path.startsWith('/special-plan/')) return true
|
||||
// role=doctor, path=/doctor/xxx → true
|
||||
// role=doctor, path=/manager/xxx → false
|
||||
return path === roleHome[role] || path.startsWith('/' + role + '/')
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</el-form>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column prop="projectNo" label="项目编号" width="160" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="280" show-overflow-tooltip />
|
||||
<el-table-column prop="currentStage" label="当前阶段" width="140">
|
||||
<template #default="{ row }">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</el-form>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column prop="projectNo" label="项目编号" width="160" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="100">
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
<el-form-item label="所属公司" prop="orgName">
|
||||
<el-input v-model="form.orgName" placeholder="所属公司" maxlength="200" :disabled="!!orgId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="部门" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="职务">
|
||||
<el-form-item label="职务" prop="position">
|
||||
<el-input v-model="form.position" placeholder="职务" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -86,7 +86,9 @@ const rules = {
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式错误', trigger: 'blur' }
|
||||
],
|
||||
orgName: [{ required: true, message: '请输入所属公司', trigger: 'blur' }]
|
||||
orgName: [{ required: true, message: '请输入所属公司', trigger: 'blur' }],
|
||||
department: [{ required: true, message: '请输入部门', trigger: 'blur' }],
|
||||
position: [{ required: true, message: '请输入职务', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<!-- ========== 筛选区 (仿 manager/meetings/Meetings.vue: 项目编号/会议ID/会议名称/期数/会议时间/项目形式/当前阶段/备注) ========== -->
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="请输入项目编号" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="请输入项目编号" clearable style="width:170px" /></el-form-item>
|
||||
<el-form-item label="会议ID"><el-input v-model="q.meetingId" placeholder="请输入会议ID" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="请输入会议名称" clearable style="width:160px" /></el-form-item>
|
||||
<el-form-item label="期数"><el-input-number v-model="q.periodNo" :min="1" placeholder="期数" style="width:140px" /></el-form-item>
|
||||
@@ -37,7 +37,7 @@
|
||||
<GrTable :data="rows" v-loading="loading" stripe border
|
||||
:main-cols="['projectNo', 'meetingName']"
|
||||
>
|
||||
<el-table-column prop="projectNo" label="项目编号" width="130" fixed />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" fixed />
|
||||
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
|
||||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
@selection-change="sel=selected=sel"
|
||||
>
|
||||
<el-table-column v-if="!isSub" type="selection" width="48" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="120" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="总场次/总期数" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.assignedSessions || 0 }}/{{ row.assignedSessions || 0 }}</template>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
@selection-change="onSel"
|
||||
>
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="140" fixed="left" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" fixed="left" />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="totalSessions" label="总场次/总期数" width="100" align="center" />
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
|
||||
@@ -126,7 +126,7 @@
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<div class="op-cell">
|
||||
<el-link :underline="false" type="primary" @click="doCreateMeeting(row)">建会</el-link>
|
||||
<el-link :underline="false" type="primary" @click="doAssign(row)">分配</el-link>
|
||||
<el-link :underline="false" type="primary" :disabled="isFinishedRow(row)" @click="doAssign(row)">分配</el-link>
|
||||
<el-dropdown trigger="hover" @command="(cmd) => onAction(cmd, row)">
|
||||
<el-link :underline="false" type="primary" class="op-dropdown">
|
||||
更多<el-icon class="op-caret"><ArrowDown /></el-icon>
|
||||
@@ -139,7 +139,6 @@
|
||||
<el-dropdown-item command="activate">开通</el-dropdown-item>
|
||||
<el-dropdown-item command="delAnn">删除公告</el-dropdown-item>
|
||||
<el-dropdown-item command="rate">执行单位评分</el-dropdown-item>
|
||||
<el-dropdown-item command="exportExperts">导出报名专家</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canDelete" command="delete">删除项目</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -282,7 +281,6 @@ function onAction(cmd, row) {
|
||||
else if (cmd === 'activate') openActivate(row)
|
||||
else if (cmd === 'delAnn') openDeleteAnnouncement(row)
|
||||
else if (cmd === 'rate') openSingleScore(row)
|
||||
else if (cmd === 'exportExperts') exportExperts(row)
|
||||
else if (cmd === 'delete') doDelete(row)
|
||||
}
|
||||
// 仅 admin 可见删除按钮 (前端 v-if, 后端 controller 再兜底 role 校验)
|
||||
@@ -291,7 +289,9 @@ const canDelete = computed(() => {
|
||||
return r === 'admin'
|
||||
})
|
||||
function doCreateMeeting(row) { router.push(`/manager/meetings/new?projectId=${row.projectId}`) }
|
||||
function isFinishedRow(row) { return row.isFinished === '1' || row.isFinished === 1 }
|
||||
function doAssign(row) {
|
||||
if (isFinishedRow(row)) return ElMessage.warning('已结题项目不能分配')
|
||||
// 跳转独立子页面 /manager/projects/assign?projectId=X
|
||||
router.push({ path: '/manager/projects/assign', query: { projectId: row.projectId } })
|
||||
}
|
||||
@@ -358,33 +358,6 @@ function readQueryFromRoute() {
|
||||
// 导出
|
||||
function exportProjects() { ElMessage.info('导出项目功能开发中') }
|
||||
function exportEval() { ElMessage.info('项目评价导出功能开发中') }
|
||||
// 导出某项目的报名专家 (biz_execution_intent)
|
||||
// 字段: 专家姓名 科室 医院 职称 报名时间 手机号
|
||||
async function exportExperts(row) {
|
||||
if (!row || !row.projectNo) { ElMessage.warning('缺少项目编号'); return }
|
||||
try {
|
||||
// 后端 BizExecutionIntent 参数没 @RequestBody, 必须走 URL query string
|
||||
const res = await request({
|
||||
url: '/business/executionIntent/export',
|
||||
method: 'post',
|
||||
params: { projectNo: row.projectNo },
|
||||
responseType: 'blob'
|
||||
})
|
||||
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `报名专家_${row.projectNo}.xlsx`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (e) {
|
||||
const msg = e?.msg || e?.message || '导出失败'
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 单行操作按钮 handlers(弹 5 个独立 dialog) ==========
|
||||
function openClose(row) {
|
||||
@@ -527,15 +500,20 @@ function openAssign() {
|
||||
ElMessage.warning('请先勾选项目')
|
||||
return
|
||||
}
|
||||
// 已结题项目不能分配: 过滤掉已结题项, 并提示跳过数
|
||||
const targets = selection.value.filter(r => !isFinishedRow(r))
|
||||
if (!targets.length) return ElMessage.warning('所选项目均已结题, 不能分配')
|
||||
const skipCount = selection.value.length - targets.length
|
||||
// 跳转独立子页面 /manager/projects/assign, 单条 vs 批量通过 query 区分:
|
||||
// 单条: ?projectId=X → 子页面 v-if 单条分支
|
||||
// 批量: ?projectIds=1,2,3 → 子页面 v-else 批量分支
|
||||
if (selection.value.length === 1) {
|
||||
router.push({ path: '/manager/projects/assign', query: { projectId: selection.value[0].projectId } })
|
||||
if (targets.length === 1) {
|
||||
router.push({ path: '/manager/projects/assign', query: { projectId: targets[0].projectId } })
|
||||
} else {
|
||||
const ids = selection.value.map(r => r.projectId).join(',')
|
||||
const ids = targets.map(r => r.projectId).join(',')
|
||||
router.push({ path: '/manager/projects/assign', query: { projectIds: ids } })
|
||||
}
|
||||
if (skipCount > 0) ElMessage.warning(`已跳过 ${skipCount} 个已结题项目`)
|
||||
}
|
||||
function openBatch(kind) {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
|
||||
|
||||
@@ -299,75 +299,70 @@
|
||||
<div class="timeline-title">会议已执行</div>
|
||||
<div class="timeline-desc">{{ nodeDesc('PRE') }}</div>
|
||||
</div>
|
||||
<!-- 节点 2: 审核时间轴 (内嵌 提交 → 合规 → 监察 三步, 两轨并列, 保留重交历史) -->
|
||||
<div :class="['timeline-item', auditNodeStatus()]">
|
||||
<div class="timeline-title">审核时间轴</div>
|
||||
<div class="audit-sub-timeline">
|
||||
<div :class="['sub-timeline-item', nodeStatus('submit')]">
|
||||
<div class="sub-timeline-title">执行方提交材料</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待执行人员提交</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">待提交</span>
|
||||
</div>
|
||||
<div v-for="(c, i) in track.cycles" :key="i" class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<span>{{ c.submit.auditor }}</span>
|
||||
<!-- 材料审核三步 (提交 → 合规 → 监察) 并入主时间轴, 两轨并列, 保留重交历史 -->
|
||||
<div :class="['timeline-item', nodeStatus('submit')]">
|
||||
<div class="timeline-title">执行方提交材料</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待执行人员提交</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">待提交</span>
|
||||
</div>
|
||||
<div v-for="(c, i) in track.cycles" :key="i" class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<span>{{ c.submit.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.submit.auditTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['timeline-item', nodeStatus('compliance')]">
|
||||
<div class="timeline-title">合规审核</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待合规审核</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">—</span>
|
||||
</div>
|
||||
<template v-for="(c, i) in track.cycles" :key="i">
|
||||
<div class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<template v-if="c.compliance">
|
||||
<span>{{ c.compliance.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.submit.auditTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['sub-timeline-item', nodeStatus('compliance')]">
|
||||
<div class="sub-timeline-title">合规审核</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待合规审核</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">—</span>
|
||||
</div>
|
||||
<template v-for="(c, i) in track.cycles" :key="i">
|
||||
<div class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<template v-if="c.compliance">
|
||||
<span>{{ c.compliance.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.compliance.auditTime) }}</span>
|
||||
<el-tag size="small" :type="c.compliance.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.compliance.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</template>
|
||||
<span v-else class="pending-text">待审核</span>
|
||||
</div>
|
||||
<div v-if="c.compliance?.opinion" class="timeline-opinion">💬 {{ c.compliance.opinion }}</div>
|
||||
<span>{{ fmtDateTime(c.compliance.auditTime) }}</span>
|
||||
<el-tag size="small" :type="c.compliance.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.compliance.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</template>
|
||||
<span v-else class="pending-text">待审核</span>
|
||||
</div>
|
||||
<div v-if="c.compliance?.opinion" class="timeline-opinion">💬 {{ c.compliance.opinion }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['timeline-item', nodeStatus('supervision')]">
|
||||
<div class="timeline-title">监察意见</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待监察审核</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">—</span>
|
||||
</div>
|
||||
<div :class="['sub-timeline-item', nodeStatus('supervision')]">
|
||||
<div class="sub-timeline-title">监察意见</div>
|
||||
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待监察审核</div>
|
||||
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||
<div class="track-label-row">
|
||||
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||
<span v-if="!track.cycles.length" class="pending-text">—</span>
|
||||
</div>
|
||||
<template v-for="(c, i) in track.cycles" :key="i">
|
||||
<div class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<template v-if="c.compliance && c.compliance.auditResult === 'REJECTED'">
|
||||
<span class="pending-text">已退回 · 未进入监察</span>
|
||||
</template>
|
||||
<template v-else-if="c.supervision">
|
||||
<span>{{ c.supervision.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.supervision.auditTime) }}</span>
|
||||
<el-tag size="small" :type="c.supervision.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.supervision.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</template>
|
||||
<span v-else class="pending-text">待监察审核</span>
|
||||
</div>
|
||||
<div v-if="c.supervision?.opinion" class="timeline-opinion">💬 {{ c.supervision.opinion }}</div>
|
||||
<template v-for="(c, i) in track.cycles" :key="i">
|
||||
<div class="timeline-meta">
|
||||
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||
<template v-if="c.compliance && c.compliance.auditResult === 'REJECTED'">
|
||||
<span class="pending-text">已退回 · 未进入监察</span>
|
||||
</template>
|
||||
<template v-else-if="c.supervision">
|
||||
<span>{{ c.supervision.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.supervision.auditTime) }}</span>
|
||||
<el-tag size="small" :type="c.supervision.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.supervision.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</template>
|
||||
<span v-else class="pending-text">待监察审核</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="c.supervision?.opinion" class="timeline-opinion">💬 {{ c.supervision.opinion }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 节点 3/4: 结算 / 完结 -->
|
||||
@@ -421,14 +416,18 @@
|
||||
|
||||
<!-- 参会人 dialog (新增/编辑通用, 2 列布局) -->
|
||||
<el-dialog v-model="attendeeDialog.show" :title="attendeeDialog.title" width="760px" append-to-body :close-on-click-modal="false" @closed="resetAttendeeForm">
|
||||
<el-form :model="attendeeDialog.form" label-width="100px" class="attendee-form-2col">
|
||||
<el-form ref="attendeeFormRef" :model="attendeeDialog.form" :rules="attendeeRules" label-width="100px" class="attendee-form-2col">
|
||||
<!-- Col 1: 基础档案 (8 字段) -->
|
||||
<el-form-item label="联系方式" required>
|
||||
<el-input v-model="attendeeDialog.form.phone" :disabled="attendeeDialog.editing" placeholder="11位手机号" maxlength="11">
|
||||
<template v-if="!attendeeDialog.editing" #suffix>
|
||||
<el-icon class="phone-lookup" title="按手机号查专家并回填" @click="lookupExpertByPhone"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="attendeeDialog.form.name" placeholder="请输入姓名(未注册手机号将以此作为昵称建号)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系方式" required>
|
||||
<el-input v-model="attendeeDialog.form.phone" :disabled="attendeeDialog.editing" placeholder="11位手机号" maxlength="11" />
|
||||
</el-form-item>
|
||||
<el-form-item label="医院">
|
||||
<el-input v-model="attendeeDialog.form.workUnit" placeholder="请输入医院名称" />
|
||||
</el-form-item>
|
||||
@@ -475,7 +474,7 @@
|
||||
<el-form-item label="增值税附加">
|
||||
<el-input-number v-model="attendeeDialog.form.vatAndSurcharge" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="实发金额">
|
||||
<el-form-item label="实发金额" prop="fee">
|
||||
<el-input-number v-model="attendeeDialog.form.fee" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="摘要">
|
||||
@@ -570,7 +569,7 @@ import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
||||
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
|
||||
import ProjectRoleMultiSelect from '@/components/ProjectRoleMultiSelect.vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { Upload, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { Upload, ArrowDown, Search } from '@element-plus/icons-vue'
|
||||
import { derivePhysicalStage } from '@/utils/meetingStage'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -783,12 +782,6 @@ function nodeDesc(slot) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
/** 审核时间轴 组节点状态: 两轨均审核通过 (进入待结算) 后 done, 否则 pending (内嵌子步骤各自带色). */
|
||||
function auditNodeStatus() {
|
||||
const s = derivePhysicalStage(row.value)
|
||||
return ['AWAITING_SETTLEMENT', 'SETTLED', 'FINISHED'].includes(s) ? 'done' : 'pending'
|
||||
}
|
||||
|
||||
// ===================== 按钮显隐 =====================
|
||||
// 执行方判定: 用 role (executor) 而非 biz_meeting_executor (会议级执行人员).
|
||||
// 执行单位是项目级分配 (biz_project_assign), 不在 biz_meeting_executor 里, 旧判定会让执行单位在"执行中"看不到提交按钮.
|
||||
@@ -937,6 +930,50 @@ function resetAttendeeForm() {
|
||||
show: false, title: '', editing: false,
|
||||
form: emptyAttendeeForm(), saving: false
|
||||
}
|
||||
// 清掉上次的校验错误 (防止关掉后重开还残留"实发金额不能超过..."红字)
|
||||
attendeeFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
/** 参会人表单 ref (el-form validate 用) */
|
||||
const attendeeFormRef = ref()
|
||||
|
||||
/** 计算所选角色(可多个, 逗号分隔)的项目劳务金额合计; 返回 { sum, matchedAny } */
|
||||
function roleAmountSum(laborForm) {
|
||||
const result = { sum: 0, matchedAny: false }
|
||||
if (!laborForm) return result
|
||||
const items = String(laborForm).split(',').map(s => s.trim()).filter(Boolean)
|
||||
items.forEach(item => {
|
||||
const matched = (projectRoles.value || []).find(r => {
|
||||
if (!r) return false
|
||||
const label = r.role === '其他' ? (r.customName || '').trim() : r.role
|
||||
return label === item
|
||||
})
|
||||
if (matched && matched.amount != null) {
|
||||
result.sum += Number(matched.amount) || 0
|
||||
result.matchedAny = true
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/** 实发金额校验: 不超所选角色的劳务金额合计 (未选角色时跳过) */
|
||||
function validateFee(rule, value, callback) {
|
||||
const laborForm = attendeeDialog.value.form.laborForm
|
||||
if (!laborForm || !String(laborForm).trim()) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const { sum } = roleAmountSum(laborForm)
|
||||
const fee = Number(value) || 0
|
||||
if (fee > sum) {
|
||||
callback(new Error(`实发金额不能超过角色金额 ${sum.toFixed(2)} 元`))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const attendeeRules = {
|
||||
fee: [{ validator: validateFee, trigger: ['blur', 'change'] }]
|
||||
}
|
||||
|
||||
// ===================== 金额联动 (照搬 hwt guest.vue 单向链) =====================
|
||||
@@ -994,20 +1031,7 @@ watch(
|
||||
if (!val) return
|
||||
// 多选: 逗号分隔, 按每个角色名匹配项目劳务金额求和, 填进 fee (实发).
|
||||
// "其他"自定义角色不在项目列表里 → 不计入; 全"其他"时 matchedAny=false → 不动 fee.
|
||||
const items = String(val).split(',').map(s => s.trim()).filter(Boolean)
|
||||
let sum = 0
|
||||
let matchedAny = false
|
||||
items.forEach(item => {
|
||||
const matched = (projectRoles.value || []).find(r => {
|
||||
if (!r) return false
|
||||
const label = r.role === '其他' ? (r.customName || '').trim() : r.role
|
||||
return label === item
|
||||
})
|
||||
if (matched && matched.amount != null) {
|
||||
sum += Number(matched.amount) || 0
|
||||
matchedAny = true
|
||||
}
|
||||
})
|
||||
const { sum, matchedAny } = roleAmountSum(val)
|
||||
if (matchedAny) {
|
||||
attendeeDialog.value.form.fee = Number(sum.toFixed(2))
|
||||
}
|
||||
@@ -1165,6 +1189,44 @@ function openAttendeeDialog(row) {
|
||||
nextTick(() => { suppressFeeLink = false })
|
||||
}
|
||||
|
||||
/** 手机号放大镜: 按手机号查专家, 命中则回填参会人表单 (身份证/银行/科室/职称等) */
|
||||
async function lookupExpertByPhone() {
|
||||
const phone = (attendeeDialog.value.form.phone || '').trim()
|
||||
if (!phone) {
|
||||
ElMessage.warning('请先输入手机号')
|
||||
return
|
||||
}
|
||||
if (!phone.match(/^1\d{10}$/)) {
|
||||
ElMessage.warning('手机号格式不正确')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const resp = await request.get(`/business/expert/byPhone/${phone}`, { __silentError: true })
|
||||
const expert = (resp && resp.data) || null
|
||||
if (!expert) {
|
||||
ElMessage.info('未找到该手机号对应的专家')
|
||||
return
|
||||
}
|
||||
const f = attendeeDialog.value.form
|
||||
f.name = expert.name || f.name
|
||||
f.workUnit = expert.workUnit || f.workUnit
|
||||
f.department = expert.department || f.department
|
||||
f.title = expert.title || f.title
|
||||
f.idCard = expert.idCard || f.idCard
|
||||
f.bankName = expert.bankName || f.bankName
|
||||
f.bankCard = expert.bankCard || f.bankCard
|
||||
f.bankRegion = expert.bankRegion || f.bankRegion
|
||||
f.bankAddress = expert.bankAddress || f.bankAddress
|
||||
f.idCardAttachments = expert.idCardAttachments || f.idCardAttachments
|
||||
// 账户名称 = 持卡人姓名, 默认取专家姓名
|
||||
f.accountName = expert.name || f.accountName
|
||||
ElMessage.success('已回填专家信息')
|
||||
} catch (e) {
|
||||
console.error('[meeting-detail] lookupExpertByPhone failed', e)
|
||||
ElMessage.error(e?.msg || e?.message || '查询失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交 dialog (新增 → POST; 编辑 → PUT) */
|
||||
async function confirmAttendee() {
|
||||
const { editing, form } = attendeeDialog.value
|
||||
@@ -1176,6 +1238,12 @@ async function confirmAttendee() {
|
||||
ElMessage.warning('手机号格式不正确')
|
||||
return
|
||||
}
|
||||
// 校验实发金额不超所选角色劳务金额合计 (未选角色时跳过)
|
||||
try {
|
||||
await attendeeFormRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
attendeeDialog.value.saving = true
|
||||
try {
|
||||
if (editing) {
|
||||
@@ -1962,15 +2030,6 @@ onBeforeUnmount(stopFeePolling)
|
||||
.cycle-no { font-size: 11px; color: #909399; }
|
||||
.timeline-opinion { margin-top: 4px; font-size: 12px; color: #f56c6c; background: #fef0f0; padding: 4px 8px; border-radius: 3px; line-height: 1.5; word-break: break-all; }
|
||||
.timeline-item.done .timeline-opinion { color: #909399; background: #f5f7fa; }
|
||||
/* 审核时间轴内嵌子时间轴 (提交 → 合规 → 监察) */
|
||||
.audit-sub-timeline { position: relative; padding-left: 14px; border-left: 2px solid #e8e8e8; margin: 6px 0 2px; }
|
||||
.sub-timeline-item { padding: 4px 0 10px 12px; position: relative; }
|
||||
.sub-timeline-item:last-child { padding-bottom: 0; }
|
||||
.sub-timeline-item::before { content: ''; position: absolute; left: -20px; top: 7px; width: 8px; height: 8px; border-radius: 50%; background: var(--brand-primary); }
|
||||
.sub-timeline-item.done::before { background: #67c23a; }
|
||||
.sub-timeline-item.pending::before { background: #c0c4cc; }
|
||||
.sub-timeline-item.rejected::before { background: #f56c6c; box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2); }
|
||||
.sub-timeline-title { font-size: 12px; font-weight: 600; color: #303133; margin-bottom: 4px; line-height: 1.4; }
|
||||
|
||||
.audit-columns { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; min-width: 0; }
|
||||
.audit-column { margin-bottom: 0; padding: 16px 18px; min-width: 0; }
|
||||
@@ -1987,6 +2046,9 @@ onBeforeUnmount(stopFeePolling)
|
||||
.attendee-form-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px; }
|
||||
.attendee-form-2col :deep(.el-form-item) { margin-bottom: 12px; }
|
||||
.attendee-form-2col :deep(.el-form-item__content) { min-width: 0; }
|
||||
/* 手机号放大镜 suffix: 可点 + hover 高亮 */
|
||||
.phone-lookup { cursor: pointer; color: var(--el-text-color-secondary); transition: color 0.2s; }
|
||||
.phone-lookup:hover { color: var(--brand-primary, #409eff); }
|
||||
/* 表格外层横向滚动容器: 内容总宽 ~1680px 超出 left-col 宽度时, 容器内出现横向滚动条, 不撑爆外层 grid */
|
||||
.attendee-table-wrap { overflow-x: auto; max-width: 100%; min-width: 0; }
|
||||
.row-actions { display: inline-flex; align-items: center; gap: 4px; }
|
||||
@@ -2161,8 +2223,6 @@ onBeforeUnmount(stopFeePolling)
|
||||
/* 时间轴缩窄 */
|
||||
.timeline { padding-left: 18px !important; }
|
||||
.timeline-item::before { left: -23px !important; }
|
||||
.audit-sub-timeline { padding-left: 12px !important; }
|
||||
.sub-timeline-item::before { left: -18px !important; }
|
||||
|
||||
/* 顶部"返回/操作"按钮组横滚 */
|
||||
.page-title { flex-wrap: wrap !important; gap: 8px !important; }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<!-- ========== 筛选区 (与 People.vue 风格一致) ========== -->
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="请输入项目编号" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="请输入项目编号" clearable style="width:170px" /></el-form-item>
|
||||
<el-form-item label="会议ID"><el-input v-model="q.meetingId" placeholder="请输入会议ID" clearable style="width:140px" /></el-form-item>
|
||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="请输入会议名称" clearable style="width:160px" /></el-form-item>
|
||||
<el-form-item label="期数"><el-input-number v-model="q.periodNo" :min="1" placeholder="期数" style="width:140px" /></el-form-item>
|
||||
@@ -46,7 +46,7 @@
|
||||
:main-cols="['projectNo', 'meetingName']"
|
||||
>
|
||||
<el-table-column v-if="isRole('admin', 'manager')" type="selection" width="44" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="130" fixed />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" fixed />
|
||||
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
|
||||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
|
||||
|
||||
@@ -1,87 +1,6 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<header class="top-nav" :class="topNavClass">
|
||||
<a class="logo" title="返回首页" @click.prevent="onHome">
|
||||
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-title">北京整合医学学会</span>
|
||||
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item"><span class="nav-link active">年度项目规划</span></li>
|
||||
<li class="nav-item"><a class="nav-link" @click.prevent="goPublicity">项目公示</a></li>
|
||||
</ul>
|
||||
<div class="top-tools">
|
||||
<template v-if="!loggedIn">
|
||||
<span class="login-btn" @click="goLogin">登录</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-dropdown trigger="click" @command="onUserCmd">
|
||||
<a class="user-link" @click.prevent>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<span>{{ userName }}</span>
|
||||
</a>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="account">我的主页</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 手机端汉堡按钮 (桌面隐藏) -->
|
||||
<button class="hamburger" type="button" aria-label="打开菜单" @click="drawerOpen = true">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- 手机端抽屉菜单 (桌面隐藏) -->
|
||||
<transition name="drawer-fade">
|
||||
<div v-if="drawerOpen" class="drawer-mask" @click="drawerOpen = false"></div>
|
||||
</transition>
|
||||
<transition name="drawer-slide">
|
||||
<aside v-if="drawerOpen" class="drawer-panel" role="dialog" aria-label="导航菜单">
|
||||
<div class="drawer-header">
|
||||
<span class="drawer-title">导航菜单</span>
|
||||
<button class="drawer-close" type="button" aria-label="关闭菜单" @click="drawerOpen = false">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
<line x1="6" y1="18" x2="18" y2="6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="drawer-nav">
|
||||
<a class="drawer-link" @click.prevent="goHomeAndClose">年度项目规划</a>
|
||||
<a class="drawer-link" @click.prevent="goPublicityAndClose">项目公示</a>
|
||||
</nav>
|
||||
<div class="drawer-divider"></div>
|
||||
<div class="drawer-tools">
|
||||
<template v-if="!loggedIn">
|
||||
<span class="drawer-login" @click="goLoginAndClose">登录</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="drawer-user">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<span>{{ userName }}</span>
|
||||
</div>
|
||||
<a class="drawer-link" @click.prevent="goAccountAndClose">我的主页</a>
|
||||
<a class="drawer-link danger" @click.prevent="goLogoutAndClose">退出登录</a>
|
||||
</template>
|
||||
</div>
|
||||
</aside>
|
||||
</transition>
|
||||
<PortalNavbar />
|
||||
|
||||
<div class="container">
|
||||
<section class="hero">
|
||||
@@ -135,16 +54,14 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import request from '@/utils/request'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
import PortalNavbar from '@/components/PortalNavbar.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isScrolled = ref(false)
|
||||
const specialPlans = ref([])
|
||||
const drawerOpen = ref(false)
|
||||
|
||||
async function loadSpecialPlans() {
|
||||
try {
|
||||
@@ -156,11 +73,15 @@ async function loadSpecialPlans() {
|
||||
}
|
||||
|
||||
function openPlan(id) {
|
||||
// 点击计划详情需登录: 未登录先跳登录, 登录后回跳详情
|
||||
if (!loggedIn.value) {
|
||||
ElMessage.warning('请先登录系统')
|
||||
router.push({ path: '/login', query: { redirect: `/special-plan/${id}` } })
|
||||
return
|
||||
}
|
||||
window.open(`${import.meta.env.BASE_URL}#/special-plan/${id}`, '_blank')
|
||||
}
|
||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||
const loggedIn = computed(() => !!userStore.token)
|
||||
const userName = computed(() => userStore.user?.userName || '用户')
|
||||
// 投稿角色: admin/manager 不开放投稿 → 隐藏「项目提案」按钮; 未登录 user 为 null → 显示(点击引导登录)
|
||||
const canPropose = computed(() => {
|
||||
const r = userStore.user?.role || ''
|
||||
@@ -224,42 +145,15 @@ const planSvg = `<svg viewBox="0 150 1200 370" xmlns="http://www.w3.org/2000/svg
|
||||
</g>
|
||||
</svg>`
|
||||
|
||||
function handleScroll() {
|
||||
isScrolled.value = window.scrollY > 10
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
loadSpecialPlans()
|
||||
// portal home 移动端适配: 解除 body min-width (避免手机端 1354px 横向滚动)
|
||||
document.body.classList.add('home-page-body')
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
document.body.classList.remove('home-page-body')
|
||||
})
|
||||
|
||||
function onHome() { isScrolled.value = false }
|
||||
function goLogin() { router.push('/login') }
|
||||
function goUser() { /* placeholder */ }
|
||||
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
|
||||
|
||||
// 抽屉版导航: 跳转后关闭抽屉
|
||||
function goHomeAndClose() { drawerOpen.value = false; onHome() }
|
||||
function goPublicityAndClose() { drawerOpen.value = false; goPublicity() }
|
||||
function goLoginAndClose() { drawerOpen.value = false; goLogin() }
|
||||
function goAccountAndClose() { drawerOpen.value = false; onUserCmd('account') }
|
||||
async function goLogoutAndClose() { drawerOpen.value = false; await goLogout() }
|
||||
async function onUserCmd(cmd) {
|
||||
if (cmd === 'logout') return goLogout()
|
||||
if (cmd === 'account') {
|
||||
const r = (userStore.user?.role || '')
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||
router.push(map[r] || '/admin/workbench')
|
||||
}
|
||||
}
|
||||
function goPublicity() { router.push('/publicity') }
|
||||
|
||||
function onSubmit() {
|
||||
if (!loggedIn.value) {
|
||||
ElMessage.warning('请先登录系统')
|
||||
|
||||
@@ -1,40 +1,6 @@
|
||||
<template>
|
||||
<div class="pub-page">
|
||||
<header class="top-nav" :class="topNavClass">
|
||||
<a class="logo" title="返回首页" @click.prevent="goHome">
|
||||
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-title">北京整合医学学会</span>
|
||||
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item"><a class="nav-link" @click.prevent="goHome">年度项目规划</a></li>
|
||||
<li class="nav-item"><span class="nav-link active">项目公示</span></li>
|
||||
</ul>
|
||||
<div class="top-tools">
|
||||
<template v-if="!loggedIn">
|
||||
<span class="login-btn" @click="goLogin">登录</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-dropdown trigger="click" @command="onUserCmd">
|
||||
<a class="user-link" @click.prevent>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<span>{{ userName }}</span>
|
||||
</a>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="account">我的主页</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
<PortalNavbar />
|
||||
|
||||
<main class="container">
|
||||
<div class="page-header">
|
||||
@@ -106,23 +72,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import request from '@/utils/request'
|
||||
import { bizList } from '@/api/public'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
import PortalNavbar from '@/components/PortalNavbar.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const isScrolled = ref(false)
|
||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||
const loggedIn = computed(() => !!userStore.token)
|
||||
const userName = computed(() => userStore.user?.userName || '用户')
|
||||
|
||||
const ALL_NOTICES = []
|
||||
|
||||
const currentKeyword = ref('')
|
||||
@@ -203,19 +162,6 @@ function prevPage() { if (currentPage.value > 1) { currentPage.value--; window.s
|
||||
function nextPage() { if (currentPage.value < totalPages.value) { currentPage.value++; window.scrollTo({ top: 0, behavior: 'smooth' }) } }
|
||||
function goPage(p) { currentPage.value = p; window.scrollTo({ top: 0, behavior: 'smooth' }) }
|
||||
|
||||
function goHome() { router.push('/') }
|
||||
function goPublicity() { router.push('/publicity') }
|
||||
function goLogin() { router.push('/login') }
|
||||
function goUser() { /* placeholder */ }
|
||||
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
|
||||
async function onUserCmd(cmd) {
|
||||
if (cmd === 'logout') return goLogout()
|
||||
if (cmd === 'account') {
|
||||
const r = (userStore.user?.role || '')
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||
router.push(map[r] || '/admin/workbench')
|
||||
}
|
||||
}
|
||||
function goDetail(n) {
|
||||
if (!n || !n.annId) return
|
||||
router.push({ name: 'publicity-detail', params: { projectId: n.annId } })
|
||||
@@ -229,9 +175,6 @@ function fmtDate(d) {
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||
onMounted(() => window.addEventListener('scroll', handleScroll, { passive: true }))
|
||||
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,40 +1,6 @@
|
||||
<template>
|
||||
<div class="detail-page">
|
||||
<header class="top-nav" :class="topNavClass">
|
||||
<a class="logo" title="返回首页" @click.prevent="goHome">
|
||||
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-title">北京整合医学学会</span>
|
||||
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item"><a class="nav-link" @click.prevent="goHome">年度项目规划</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" @click.prevent="goPublicity">项目公示</a></li>
|
||||
</ul>
|
||||
<div class="top-tools">
|
||||
<template v-if="!loggedIn">
|
||||
<span class="login-btn" @click="goLogin">登录</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-dropdown trigger="click" @command="onUserCmd">
|
||||
<a class="user-link" @click.prevent>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<span>{{ userName }}</span>
|
||||
</a>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="account">我的主页</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
<PortalNavbar />
|
||||
|
||||
<main class="container">
|
||||
<a class="back-link" @click.prevent="goPublicity">
|
||||
@@ -183,8 +149,8 @@ import { ref, computed, watch, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import PortalFooter from '@/components/PortalFooter.vue'
|
||||
import PortalNavbar from '@/components/PortalNavbar.vue'
|
||||
import { listBizPerson } from '@/api/business/person'
|
||||
import { bizGet } from '@/api/public'
|
||||
import {
|
||||
@@ -199,10 +165,7 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isScrolled = ref(false)
|
||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||
const loggedIn = computed(() => !!userStore.token)
|
||||
const userName = computed(() => userStore.user?.userName || '用户')
|
||||
// 意向按钮按角色显隐:
|
||||
// sponsor (含 MAIN/SUB) → 仅支持意向; executor (含 MAIN/SUB) → 仅执行意向
|
||||
// admin / manager / doctor → 两个都隐藏
|
||||
@@ -359,19 +322,7 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function goHome() { router.push('/') }
|
||||
function goPublicity() { router.push('/publicity') }
|
||||
function goLogin() { router.push('/login') }
|
||||
function goUser() { /* placeholder */ }
|
||||
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
|
||||
async function onUserCmd(cmd) {
|
||||
if (cmd === 'logout') return goLogout()
|
||||
if (cmd === 'account') {
|
||||
const r = (userStore.user?.role || '')
|
||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||
router.push(map[r] || '/admin/workbench')
|
||||
}
|
||||
}
|
||||
|
||||
const signed = ref(false)
|
||||
async function checkSigned() {
|
||||
@@ -572,7 +523,6 @@ function onViewBidAnnouncement() {
|
||||
}
|
||||
|
||||
function onKeydown(e) { if (e.key === 'Escape') showQr.value = false }
|
||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||
|
||||
// 下载二维码: qrUrl 是 QRCode.toDataURL 生成的 data:image/png;base64,... 直接 <a download> 即可
|
||||
function onDownloadQr() {
|
||||
@@ -586,12 +536,10 @@ function onDownloadQr() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
load()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -147,6 +147,11 @@ async function onUserCmd(cmd) {
|
||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||
|
||||
onMounted(() => {
|
||||
// 计划详情需登录 (点击验证登录状态): 未登录跳登录, 登录后回跳 (兜底拦截直链访问)
|
||||
if (!loggedIn.value) {
|
||||
router.replace({ path: '/login', query: { redirect: route.fullPath } })
|
||||
return
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
load()
|
||||
})
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
<el-form-item label="所属公司" prop="orgName">
|
||||
<el-input v-model="form.orgName" placeholder="所属公司" maxlength="200" :disabled="!!orgId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="部门" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="职务">
|
||||
<el-form-item label="职务" prop="position">
|
||||
<el-input v-model="form.position" placeholder="职务" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -78,7 +78,9 @@ const rules = {
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: '手机号格式错误', trigger: 'blur' }
|
||||
],
|
||||
orgName: [{ required: true, message: '请输入所属公司', trigger: 'blur' }]
|
||||
orgName: [{ required: true, message: '请输入所属公司', trigger: 'blur' }],
|
||||
department: [{ required: true, message: '请输入部门', trigger: 'blur' }],
|
||||
position: [{ required: true, message: '请输入职务', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
:main-cols="['projectNo', 'meetingName']"
|
||||
>
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="projectNo" label="项目编号" min-width="130" />
|
||||
<el-table-column prop="projectNo" label="项目编号" min-width="170" />
|
||||
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="总期数" prop="totalPeriod" width="80" />
|
||||
<el-table-column label="期数" prop="periodNo" width="70" />
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
:main-cols="['projectNo', 'projectName']"
|
||||
>
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="projectNo" label="项目编号" min-width="140" fixed>
|
||||
<el-table-column prop="projectNo" label="项目编号" min-width="170" fixed>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false" @click="openDetail(row)">{{ row.projectNo }}</el-link></template></el-table-column>
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
>
|
||||
<!-- SUB 子账号无需勾选 (不参与分配) -->
|
||||
<el-table-column v-if="!isSub" type="selection" width="40" />
|
||||
<el-table-column label="项目编号" width="160">
|
||||
<el-table-column label="项目编号" width="170">
|
||||
<template #default="{ row }">
|
||||
<a class="link">{{ row.projectNo }}</a>
|
||||
</template>
|
||||
|
||||
@@ -29,11 +29,11 @@ export default defineConfig(({ mode }) => {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
[apiPath]: {
|
||||
// 远程后端: risingdoctor.com/hg-api (HTTPS, 远程 RuoYi v3.9.2)
|
||||
target: 'https://risingdoctor.com',
|
||||
// 本地后端: localhost:8080 (RuoYi context-path=/, 去掉 /dev-api 前缀直连)
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
rewrite: (p) => p.replace(new RegExp('^' + apiPath), '/hg-api')
|
||||
secure: false,
|
||||
rewrite: (p) => p.replace(new RegExp('^' + apiPath), '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user