最近在编写前端界面 ,硬是一人一周时间加班加点写完了一个项目的前端界面(一级菜单有12个页面+一个控制台大屏 ,二三级界面有N个) ,之前预估前端界面的编写需要一个月 ,我是自己把自己卷死了(没有办法 ,项目经理说项目要1周写界面 ,2周发版 ,我这个项目前端只我1个人 ,后端有3个人...).......我只想说的是 ,即便你编码速度再快 ,也无可避免地面临加班 ,即便你编码的速度能赶上产品做界面的速度 ,你也赶不上产品改界面需求的速度 。因为很多公司就是给你直接先定时间,然后反推 ,又不加人又不减需求 ,能咋办呢?下面要说的是某一个界面中的一个站点配置模块 。
产品原型
需求说明:
【站点配置】
1 、站点配置,需要线路属性配置完后才可以配置 ,编辑的时候做校验 ,线路属性没有配置完 ,不可以编辑 ,禁用状态 。
2 、上下行:需要选择配置上行还是下行的站点 ,单选 。是否有上下行 ,需要根据线路属性来判断 。
2 、统计:统计上行或下行站点数量 ,大站数量
3 、站点配置:
3.1 、场站:根据线路属性配置的上下行场站 ,如果为上行 ,上行场站在前下行场站在后 ,在图中显示场站名称 。反之 ,下行场站在前上行场站在后 。
3.2 、场站显示:上行场站 ,标记“首 ”,无“-- ”里程信息 。下行场站 ,标记“末 ” ,无“-- ”里程信息 。反之,下行场站 ,标记“首 ” ,无“-- ”里程信息 。上行场站 ,标记“末 ” ,无“-- ”里程信息
3.3 、场站操作:“首 ”场站选中后 ,左侧显示“+ ” ,可以添加第一个站点 。“末 ”场站选中后 ,由侧显示“+ ” ,可以添加倒数第一个站点 。
3.4 、站点显示:显示序号 、站间距 、时长。站间距根据地图计算 ,时长需要输入 ,3位整数 ,单位分钟 。
3.5 、站点移动:鼠标选中 ,移动鼠标可以拖动排序 。
3.6 、站点操作:鼠标选中后,可以往前或往后添加站点 ,也可以删除站点 ,并自动排序。
3.7、添加站点:添加空白站点,点击可以添加站点 ,按照站点名字搜索和选择 ,数据来源站点管理 。
3.8 、时长修改:不需要选中站点 ,可以直接修改时长 。
3.9 、大站设置:鼠标选中后 ,选中左上角图标 ,设置为大站点
3.10、 站点修改:不做站点的编辑操作 ,只可以删除在添加 。
3.11 、如果有添加操作 ,但未添加站点信息 。不可以保存 。提示“请添加站点信息或删除空白站点”
我做出来的界面效果
代码实现
分析:要能拖拽 ,要能动态编辑 。拖拽 ,用第三方插件vuedraggable ,布局用flex 。注意安装的是vue3版本 ,yarn add vuedraggable@next ,一开始有考虑过vue-grid-layout,后面感觉grid布局操作会更复杂 ,所以果断放弃 。
拿到产品需求时 ,先通读一遍需求,然后看一遍UI界面 ,再根据需求 ,脑袋里面快速闪现技术方案 ,如果要高效 ,尽量避免自己造轮子 ,通用技术通常都有现成轮子 ,只有定制化的功能需要自己动手 ,先把整理的解决思路和自己即将采用的技术在脑子里面推演一遍 ,当在脑子里开发完成之后 ,你就可以心无旁骛 ,然后一气呵成 ,快速编码了 。写代码也是一鼓作气 ,再而衰,三而竭!
前端项目技术栈:vue3+ts+vite3.x+element plus+wujie微前端 。
Dom代码结构:
ts代码:
<script setup lang="ts">
import draggable from "vuedraggable";
const siteList = ref<any>([
{ name: "火车站广场", id: 0, isBigSite: false }, //首站
]);
for (let i = 0; i < 30; i++) {
siteList.value.push({ name: "站点" + i, id: i + 1, isBigSite: false });
}
siteList.value.push({ name: "汽车西站", id: siteList.length + 1, isBigSite: false }); //末站
const appTags = [
{ name: "上行", id: 1 },
{ name: "下行", id: 2 },
];
const state = reactive({
drag: false,
activedIndex: -1, //当前激活项索引
activedApp: "",
totalSiteNums: 0, //总站数
bigSiteNums: 0, //大站数量
});
//获取tab类型样式
const getTagType = (tag: any) => {
return tag.id == state.activedApp ? "" : "info";
};
//点击标签
const onClickTag = (tag: any) => {
state.activedApp = tag.id;
};
const isEditSiteInfo = ref(false); //是否是编辑
//编辑信息
const onEditInfo = () => {
isEditSiteInfo.value = true;
};
//线路预览
const onLineView = () => {};
//获取固定样式
const getFixedClass = (index: number) => {
return index == 0 || index == siteList.value.length - 1;
};
//点击站点项
const onClickSiteItem = (index: number) => {
console.log("onClickSiteItem");
state.activedIndex = index;
};
//是否是首站
const isStartSite = (index: number) => {
return index == 0;
};
//是否是末站
const isEndSite = (index: number) => {
return index == siteList.value.length - 1;
};
//是否是首末站
const isStartOrEndSite = (index: number) => {
return isStartSite(index) || isEndSite(index);
};
const addSiteItem = {
name: "填写站点名称",
id: -1,
isBigSite: false,
};
//左边添加站点
const onLeftAddSite = (index: number) => {
siteList.value.splice(index, 0, addSiteItem);
};
//右边添加站点
const onRightAddSite = (index: number) => {
siteList.value.splice(index + 1, 0, addSiteItem);
};
//删除站点项
const delSiteItem = (index: number) => {
siteList.value.splice(index, 1);
};
const visible = ref(false);
const allSiteList:any = [];
for (let i = 0; i < 20; i++) {
let item: any = { name: "张三" + i, code: "00c" + i, lineName: i + 1 + "路" };
// let disabled = driverTags.value.some((s: any) => s.code == item.code);
// item.disabled = disabled;
allSiteList.push(item);
}
//选择站点
const onChangeSite = (val: any) => {
const item = allSiteList.find((f: any) => f.code == val);
siteList.value.splice(activedIndex.value, 0, item);
};
//切换大站小站
const onChangeBigSite = (index: number) => {
siteList.value[index].isBigSite = !siteList.value[index].isBigSite;
};
const { totalSiteNums, bigSiteNums, drag, activedIndex } = toRefs(state);
</script>
css代码较多 ,已独立css文件 ,css代码如下:
.operator-box {
display: flex;
width: 100%;
justify-content: space-between;
height: 36px;
align-items: center;
.tag-box {
.el-tag {
margin-right: 15px;
padding: 0px 15px;
}
}
.tools-box {
display: flex;
align-items: center;
.iconfont {
padding-right: 4px;
}
}
}
.total-info-box {
width: 100%;
display: flex;
align-items: center;
height: 44px;
color: var(--sub-title-color);
.total-num {
padding-right: 10px;
}
}
.site-list-box {
height: 488px; //2行的高度
overflow-y: auto;
}
.wrapper-site {
display: flex;
width: 100%;
flex-wrap: wrap;
gap: 20px;
margin-bottom: -4px;
}
.site-item {
width: 45px;
height: 220px;
color: var(--sub-title-color);
display: flex;
flex-direction: column; //220 - 160 -22
align-items: center;
position: relative;
margin-bottom: 4px;
.text {
width: 45px;
height: 22px;
display: flex;
justify-content: center;
align-items: center;
.time {
width: 45px;
height: 22px;
border: 1px solid #cbcdd3;
border-radius: 2px;
}
}
&.fixed {
.text {
background-color: var(--hover-font-color);
color: #f7f9f7;
font-size: 12px;
}
}
.distance {
height: 38px;
color: #4f5a68;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
}
.content {
height: 160px;
background-color: #f7f9f7;
writing-mode: vertical-rl;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
position: relative;
.site-name {
writing-mode: vertical-rl;
text-orientation: upright; /* 数字垂直展示 */
// position:absolute;left:50%;top:50%;--webkit-transform:translate(-50%,-50%);
}
.site-index {
width: 20px;
height: 20px;
border-radius: 2px;
background: #e4e6ec;
color: var(--sub-title-color);
writing-mode: horizontal-tb;
text-align: center;
line-height: 20px;
}
$bw: 10px;
.triangle-block {
display: inline-block;
height: 0;
width: 0;
border-top: $bw solid #faad14;
border-bottom: $bw solid transparent;
border-left: $bw solid transparent;
border-right: $bw solid transparent;
transform: rotate(135deg);
transform-origin: top;
position: absolute;
top: 6px;
left: -4px;
&.edit {
border-top: $bw solid #004eff;
}
}
}
.del-btn {
display: flex;
justify-content: center;
height: 24px;
align-items: center;
width: 24px;
position: absolute;
bottom: -24px;
left: calc(50% - 12px);
&:hover {
.iconfont {
color: red;
}
}
}
//左右加号
.plus {
display: none;
position: absolute;
z-index: 9;
top: calc(50% + 16px);
width: 16px;
height: 16px;
justify-content: center;
align-items: center;
background-color: var(--main-btn-color);
.iconfont {
font-size: 14px;
color: white;
}
&.left {
left: -16px;
}
&.right {
right: -16px;
}
}
.iconfont {
font-size: 14px;
display: none;
&.icon-shanchu {
color: #f53f3f;
}
}
&.actived {
.content {
border: 1px solid var(--main-btn-color);
color: var(--main-btn-color);
background: #f7faff;
}
.iconfont,
.plus {
display: flex;
}
}
}
View Code
vue组件中引入css代码:
<style lang="scss" scoped>
@import "./scss/siteSet.scss";
</style>
<style lang="scss">
.el-select {
&.select-block-mini {
width: 171.27px;
}
}
</style>
其实我花了1.5h,主要是按UI的稿子调样式 ,因为很赶 ,所以代码很糙 ,我早就有了后面重构的觉悟 ,那么短的时间内 ,几乎不可能想得很周到 ,我只能想办法在最短的时间内实现需求 。里面其实有部分功能没有实现 ,因为后端一期不现实 。最近写代码真是写得手都快抽搐了 ,即便是各种复制粘贴也累啊 ,界面实在太多了.....
注意:高德webapi2.0跟mockjs有冲突 ,启用mock会导致地图显示空白 ,但是浏览器控制又不会报错的问题!
声明:本站所有文章 ,如无特殊说明或标注,均为本站原创发布。任何个人或组织 ,在未征得本站同意时 ,禁止复制 、盗用 、采集 、发布本站内容到任何网站 、书籍等各类媒体平台 。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 。