目录结构
1,CRForm
<template>
<RightClickCopy class="CRForm">
<CRFormItem
v-for="(item, index) in items"
:key="index"
:item="item"
:row="rowMap[index]"
:col="colMap[index]"
v-model="model"
:rowCountMap="rowCountMap"
:colNumber="colNumber"
:label-width="labelWidth"
>
<template v-if="item.slot" :slot="item.slot">
<slot :name="item.slot"></slot>
</template>
</CRFormItem>
</RightClickCopy>
</template>
<script lang="ts">
import Vue, { PropType } from "vue";
import CRFormItem from "@/pc/views/CreditRating/components/CRForm/CRFormItem.vue";
import RightClickCopy from "@/pc/components/RightClickCopy.vue";
export interface CRFormItemOptions {
key: string;
label: string;
colSpan?: number; // 1
rowSpan?: number; //1
type?:
| "text"
| "highlightText"
| "input"
| "select"
| "radio"
| "textarea"
| "durationInput"
| "durationNumber";
required?: boolean;
message?: string; // 报错提示的信息
validator?: (val: any) => boolean;
highlightColor?: string; //highlightText 需要填
options?: Array<{ value: any; label: string }>; //select|radio 需要给
slot?: string; // 可自定义内容
labelTitle?: string; // 标签title
editType?: string; // 编辑的类型
dateFormat?: string; // 日期文本格式
bigNumber?: boolean; // 是否是大数字,大数字千分位格式化
longText?: boolean; // 是否是长文本
maxLength?: number; // 最大长度
textareaMaxLength?: number; // 多行文本框最大长度
formatter?: (val: any) => string; // 自定义格式化
labelSlot?: string; // label slot
ifIsError?: boolean;
placeholder?: string;
disabled?: boolean;
filterable?: boolean;
clearable?: boolean;
labelTips?: string; // label提示
labelTipsIcon?: string; // label提示icon
}
export default Vue.extend({
name: "CRForm",
components: { CRFormItem, RightClickCopy },
props: {
colNumber: {
//列数
type: Number,
default: 3
},
items: {
type: Array as PropType<Array<CRFormItemOptions>>,
required: true
},
value: {
type: Object as PropType<Record<string, any>>
},
labelWidth: {
type: String,
}
},
data() {
const model = {} as Record<string, any>;
const value = this.value;
for (const k in value) {
model[k] = value[k];
}
return {
model: model
};
},
watch: {
value: {
deep: true,
handler(v: Record<string, any>) {
for (const k in v) {
this.model[k] = v[k];
}
},
immediate: true
},
model: {
deep: true,
handler(v: Record<string, any>) {
this.$emit("input", v);
}
}
},
computed: {
rowMap(): Record<number, number> {
const rowMap: Record<number, number> = {};
let t = 0;
let row = 0;
this.items.forEach((d, i) => {
const colSpan = d.colSpan ?? 1;
if (t + colSpan > this.colNumber) {
row++;
t = 0;
}
t += colSpan;
rowMap[i] = row;
});
return rowMap;
},
colMap(): Record<number, number> {
const colMap: Record<number, number> = {};
let t = 0;
this.items.forEach((d, i, arr) => {
const colSpan = d.colSpan ?? 1;
colMap[i] = t + colSpan > this.colNumber ? 0 : t % this.colNumber;
t = colMap[i] + colSpan;
});
return colMap;
},
rowCountMap(): Record<number, number> {
const rowCountMap: Record<number, number> = {};
let r: number | undefined = undefined;
let count = 0;
this.items.forEach((d, i) => {
const row = this.rowMap[i];
if (r === undefined) {
r = row;
count = 1;
return;
}
if (r === row) {
count++;
} else {
rowCountMap[r] = count;
count = 1;
r = row;
}
});
if (r !== undefined) {
rowCountMap[r] = count;
}
return rowCountMap;
}
},
methods: {
validate(): any {
return new Promise((resolve, reject) => {
const { items, model } = this;
for (let i = 0; i < items.length; i++) {
const value = model[items[i].key];
let validate = true;
if (items[i].required) {
if (items[i].validator) {
validate = (items[i] as any).validator(value);
} else if (!value && value !== 0) {
validate = false;
}
if (!validate) {
const msg = items[i].message || "字段" + items[i].label + "为空或不符";
this.$message.error(msg);
reject();
return;
}
}
//长度校验
const maxLength = items[i].maxLength || 0;
if (maxLength && value && value.length > maxLength) {
this.$message.error(`字段${items[i].label}最多输入${maxLength}个字符`);
reject();
return;
}
}
resolve(true);
});
}
}
});
</script>
<style scoped lang="less">
.CRForm {
display: flex;
flex-wrap: wrap;
}
</style>
2. CRFormItem
<template>
<div
class="CRFormItem"
:style="itemStyles"
:data-row="row"
:data-col="col"
:class="{ 'is-error': isError }"
>
<div class="label" :title="item.labelTitle" :style="{width: labelWidth}">
<span class="text">{{ item.label }}</span>
<span v-if="item.labelTips" class="required" :title="item.labelTips">
<i :class="item.labelTipsIcon || 'el-icon-question'" />
</span>
<template v-if="type !== 'text' && type !== 'highlightText' && type !== 'tag'">
<span v-if="item.required" class="required" title="必填项">
<i class="el-icon-warning" />
</span>
<!-- <span v-else class="not-required" title="选填项">-->
<!-- <i class="el-icon-info" />-->
<!-- </span>-->
</template>
</div>
<div class="field" :id="'field-' + item.key">
<template v-if="item.slot">
<slot :name="item.slot"></slot>
</template>
<template v-else>
<template v-if="type === 'text'">
<div :title="item.longText ? '' : itemText" class="field-text" :style="getLineClamp(item)" @dblclick="showTextDialog" :data-text="itemText">
{{ itemText }}
</div>
</template>
<template v-if="type === 'highlightText'">
<div class="field-text highlight" :style="getLineClamp(item)" :title="item.longText ? '' : itemText" @dblclick="showTextDialog" :data-text="itemText">
<span :style="{ fontWeight: 700, color: item.highlightColor }">{{itemText}}</span>
</div>
</template>
<template v-if="type === 'tag'">
<div
:title="item.longText ? '' : itemText"
class="field-text"
:style="getLineClamp(item)"
@dblclick="showTextDialog"
:data-text="itemText"
>
<el-tag size="mini" color="#ff0000" style="font-weight: bold; color: #ffffff">
{{ itemText }}
</el-tag>
</div>
</template>
<template v-if="type === 'number'">
<el-input-number :controls="false" size="mini" v-model="model[item.key]" :precision="item.precision" clearable
:placeholder="item.placeholder || '请输入'"
:disabled="item.disabled || false"
>
</el-input-number>
</template>
<template v-if="type === 'input'">
<el-input size="mini" v-model="model[item.key]" clearable :placeholder="item.placeholder || '请输入'" :disabled="item.disabled || false">
<template slot="append" v-if="item.append">{{ item.append }}</template>
</el-input>
</template>
<template v-if="type === 'date'">
<el-date-picker
v-model="model[item.key]"
type="date"
size="mini"
format="yyyy-MM-dd"
:value-format="item.valueFormat"
placeholder="选择日期"
:disabled="item.disabled || false"
clearable>
</el-date-picker>
</template>
<template v-if="type === 'textarea'">
<el-input
type="textarea"
size="mini"
resize="none"
v-model="model[item.key]"
:disabled="item.disabled || false"
:autosize="getTextAreaRows(item.rowSpan)"
:maxLength="item.textareaMaxLength || null"
/>
</template>
<template v-if="type === 'select'">
<el-select size="mini" v-model="model[item.key]" :filterable="item.filterable || false" :clearable="item.clearable" :placeholder="item.placeholder" :disabled="item.disabled || false">
<el-option
v-for="(optionItem, index) in item.options"
:key="index"
:label="optionItem.label"
:value="optionItem.value"
/>
</el-select>
</template>
<template v-if="type === 'multiselect'">
<el-select collapse-tags size="mini" filterable multiple v-model="model[item.key]" clearable :disabled="item.disabled || false">
<el-option
v-for="(optionItem, index) in item.options"
:key="index"
:label="optionItem.label"
:value="optionItem.value"
/>
</el-select>
</template>
<template v-if="type === 'radio'">
<el-radio-group size="mini" v-model="model[item.key]" :disabled="item.disabled || false">
<el-radio
v-for="(optionItem, index) in item.options"
:key="index"
:label="optionItem.value"
>
{{ optionItem.label }}
</el-radio>
</el-radio-group>
</template>
<template v-if="type === 'durationInput'">
<div class="durationInput">
<el-input size="mini" v-model="model[item.key][0]" clearable />
<span>~</span>
<el-input size="mini" v-model="model[item.key][1]" clearable />
</div>
</template>
<template v-if="type === 'durationNumber'">
<div class="durationNumber">
<el-input-number
v-model="model[item.key][0]"
:min="0"
size="mini"
clearable
></el-input-number>
<!-- <el-input size="mini" v-model="model[item.key][0]" clearable /> -->
<span>~</span>
<el-input-number
v-model="model[item.key][1]"
:min="0"
size="mini"
clearable
></el-input-number>
</div>
</template>
<!-- <Renderer-->
<!-- :render="fieldRender"-->
<!-- :props="item"-->
<!-- />-->
</template>
</div>
<el-dialog :title="item.label" :visible.sync="textDialogVisible">
<div class="long-text-content" @contextmenu.prevent.stop="onRightClick">
{{ textDialogContent }}
</div>
</el-dialog>
</div>
</template>
<script lang="tsx">
import Vue, { PropType } from "vue";
import { CRFormItemOptions } from "@/pc/views/CreditRating/components/CRForm/CRForm.vue";
import {copyToClipboard, EMPTY_CHAR, isEmpty} from "@/pc/utils";
import moment from "moment";
import { numberFormatter } from "@/pc/utils";
export default Vue.extend({
name: "CRFormItem",
components: {
// Renderer
},
props: {
item: {
type: Object as PropType<CRFormItemOptions>,
required: true
},
labelWidth: {
type: String,
default: "160px"
},
col: {
type: Number,
required: true
},
row: {
type: Number,
required: true
},
colNumber: {
type: Number,
required: true
},
value: {
type: Object as PropType<Record<string, any>>
},
rowCountMap: {
type: Object as PropType<Record<number, number>>,
required: true
}
},
data() {
const model = {} as Record<string, any>;
const value = this.value;
for (const k in value) {
model[k] = value[k];
}
return {
model: model,
textDialogVisible: false,
textDialogContent: ""
};
},
watch: {
value: {
deep: true,
handler(v: Record<string, any>) {
for (const k in v) {
this.model[k] = v[k];
}
},
immediate: true
},
model: {
deep: true,
handler(v: Record<string, any>) {
this.$emit("input", v);
}
},
},
computed: {
isError() {
const value = this.model[this.item.key];
if (this.item.required) {
if (value === "" || value === null || value === undefined) {
return this.item.ifIsError ? false : true;
//return true;
}
}
if (this.item.validator) {
if (!this.item.validator(value)) {
return true;
}
}
return false;
},
itemStyles(): any {
const baseHeight = 30;
const rowSpan = this.item.rowSpan ?? 1;
const colSpan = Math.min(this.colNumber, this.item.colSpan ?? 1);
return {
height: `${baseHeight * rowSpan}px`,
width: `calc((100% - ${this.rowCountMap[this.row] + 1}px)/${this.colNumber}*${colSpan})`
};
},
type(): CRFormItemOptions["type"] {
return this.item.type ?? "text";
},
itemText(): string {
const { item } = (this as any);
const value = (this as any).model[item.key];
if (item.formatter) {
return item.formatter(value);
}
if (item.editType === "select" || item.editType === "radio") {
const options = item.options || [];
for (let i = 0; i < options.length; i++) {
if (options[i].value === value) {
return options[i].label;
}
}
}
if (item.editType === "multiselect" ) {
const options = item.options || [];
const labels : any = []
if(value.length>0){
value.forEach(item => {
for (let i = 0; i < options.length; i++) {
if (options[i].value === item) {
labels.push(options[i].label);
}
}
})
return labels.join(',')
} else {
return "-"
}
}
if (item.editType === "date") {
if (value) {
return moment(value).format(item.dateFormat || "YYYY-MM-DD");
}
}
if (item.bigNumber) {
return this.formatBigNumber(value);
}
return this.formatter(value);
}
},
methods: {
showTextDialog(): any {
if (!this.item.longText) {
return;
}
this.textDialogVisible = true;
this.textDialogContent = this.model[this.item.key];
},
formatBigNumber(value: number): any {
return numberFormatter(value);
},
getTextAreaRows(rowSpan: any): any {
rowSpan = rowSpan || 1;
rowSpan = parseInt(((rowSpan * 30 - 10) / 20) + "");
return { minRows: rowSpan, maxRows: rowSpan }
},
getLineClamp(item: any): any {
if (item.longText) {
// 长文本加滚动条
return {height: "100%", width: "100%", whiteSpace: "normal", overflow: "auto"};
}
let rowSpan = item.rowSpan || 1;
rowSpan = parseInt(((rowSpan * 30 - 10) / 20) + "");
const height = rowSpan * 20;
return {"-webkit-line-clamp": rowSpan, height: `${height}px`};
},
formatter(val: any) {
if (isEmpty(val)) return EMPTY_CHAR;
return val;
},
getItemText(value, item): string {
if (item.formatter) {
return item.formatter(value);
}
if (item.editType === "select" || item.editType === "radio") {
const options = item.options || [];
if(value == null || value === ""){
return "-"
}
for (let i = 0; i < options.length; i++) {
if (options[i].value === value) {
return options[i].label;
}
}
}
if (item.editType === "multiselect" ) {
const options = item.options || [];
const labels : any = []
if(value.length>0){
value.forEach(item => {
for (let i = 0; i < options.length; i++) {
if (options[i].value === item) {
labels.push(options[i].label);
}
}
})
return labels.join(',')
} else {
return "-"
}
}
if (item.editType === "date") {
if (value) {
return moment(value).format(item.dateFormat || "YYYY-MM-DD");
}
}
if (item.bigNumber) {
return this.formatBigNumber(value);
}
return this.formatter(value);
},
onRightClick(e) {
const item = (this as any).item;
// 文本,双击复制
if (item.type === "text" || item.type === "highlightText") {
const value = this.model[item.key];
if (value || value == 0) {
if (copyToClipboard(value)) {
this.$message.info("复制成功");
} else {
this.$message.error("复制失败");
}
}
}
},
}
});
</script>
<style scoped lang="less">
@border-style: 1px solid rgb(235, 238, 245);
.CRFormItem {
display: flex;
align-items: stretch;
font-size: 12px;
line-height: 14px;
flex-wrap: nowrap;
//box-sizing: border-box;
border: @border-style;
//border-top: none;
//border-left: none;
&[data-row="0"] {
margin-top: 0;
}
&[data-col="0"] {
margin-left: 0;
}
/deep/.el-input-number .el-input__inner{
text-align: left;
}
margin-top: -1px;
margin-left: -1px;
&.is-error {
::v-deep .el-input__inner,
::v-deep .el-textarea__inner {
border-color: #731e00 !important;
}
}
> .label {
background: #f8f8f8;
color: #731e00;
padding: 0 10px;
box-sizing: border-box;
display: flex;
align-items: center;
flex-shrink: 0;
flex-grow: 0;
.text {
flex-grow: 1;
min-width: 0;
}
.required {
color: red;
flex-shrink: 0;
}
.not-required {
color: rgb(245, 169, 75);
flex-shrink: 0;
}
}
> .field {
border-left: @border-style;
padding: 0 5px;
min-width: 0;
flex-grow: 1;
display: flex;
align-items: center;
> * {
width: 100%;
}
.field-text {
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
height: calc(100% - 10px);
}
.highlight {
color: #731e00;
}
}
.durationInput {
display: inline-block;
> span {
text-align: center;
margin: 0 4px;
}
::v-deep .el-input {
width: 30%;
}
}
::v-deep .durationNumber {
display: flex;
align-items: center;
& > div {
flex: 1;
& > span {
display: none;
}
}
& > span {
padding: 0 5px;
}
.el-input__inner {
padding-left: 4px;
padding-right: 4px;
}
}
}
.long-text-content {
font-size: 12px;
line-height: 16px;
max-height: 600px;
overflow: auto;
}
</style>
3. demo
<template>
<div style="background: white;height: 100%;padding: 10px;box-sizing: border-box">
crform
<div>
<template v-for="i in 4">
<input type="radio" :id="i + 1" :value="i + 1" v-model.number="colNumber" :key="i" />
{{ i + 1 }}
</template>
</div>
<CRForm :items="items" :col-number="colNumber" :model="model" />
<br />
<CRForm :items="items1" :col-number="colNumber" :model="model1" />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import CRForm, { CRFormItemOptions } from "@/pc/views/CreditRating/components/CRForm/CRForm.vue";
export default Vue.extend({
name: "CRForm_Demo",
components: { CRForm },
data() {
const model: Record<string, any> = {};
const items: Array<CRFormItemOptions> = [...new Array(11)].map((d, i) => {
model[i] = `value${i}`;
return {
key: `${i}`,
label: `label${i}`
};
});
const model1: Record<string, any> = {};
const items1: Array<CRFormItemOptions> = [...new Array(11)].map((d, i) => {
model1[i] = `value${i}`;
let type;
let options;
if (i === 4) {
type = "textarea";
}
if (i === 3) {
type = "input";
}
if (i === 2) {
type = "select";
model1[i] = 0;
options = Array.from({ length: 4 }, (d, i) => {
return {
value: i,
label: `label${i}`
};
});
}
if (i === 6) {
type = "radio";
model1[i] = 0;
options = Array.from({ length: 3 }, (d, i) => {
return {
value: i,
label: `label${i}`
};
});
}
let highlightColor;
if (i === 8) {
type = "highlightText";
highlightColor = "blue";
}
let colSpan = 1;
if (i===9) {
colSpan = 2;
}
if (i===4) {
colSpan =3;
}
if (i === 10) {
console.log("aaa")
type = "durationInput";
model1[i] = ["", ""];
}
return {
key: `${i}`,
label: i == 4 ? "基础资产包括信托计划,私募基金,资管产品及其收权益" : `label${i}`,
colSpan,
type,
rowSpan: i === 4 ? 2 : 1,
required: i % 2 === 0,
options,
highlightColor
};
});
return {
colNumber: 3,
items,
model,
model1,
items1
};
}
});
</script>
<style scoped></style>
4. 使用
<CRForm :items="items" :col-number="2" v-model="form" label-width="130px">
<div slot="quotation">
<SpecilInput
:disabled="disabled"
v-model="form.quotation"
:palceHolders="['最低', '最高']"
:percentageFlag="percentageFlag"
/>
</div>
<div slot="insDate">
<AmDateRangePicker
:disabled="disabled"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
v-model="form.insDate"
class="label-item"
pickerWidth="248px"
/>
</div>
<div slot="tradeAmt">
<div class="tradeAmt">
<el-input
size="mini"
@input="tradeAmtIput"
v-model="form.tradeAmt"
placeholder="输入金额"
type="number"
></el-input>
</div>
</div>
</CRForm>
<script>
export default {
computed: {
disabled() {
return this.diaType == "update" && this.ifDisabled;
},
items() {
return [
{
label: "券面金额(万元)",
key: "tradeAmt",
colSpan: 1,
rowSpan: 1,
required: true,
type: "input",
slot: "tradeAmt",
ifIsError: true,
},
{
label: "交易方向",
key: "insType",
colSpan: 1,
rowSpan: 1,
type: "text",
required: true,
},
{
label: "报价类型",
key: "quotationType",
colSpan: 1,
rowSpan: 1,
type: "select",
disabled: this.disabled,
options: this.offerTypeOptions,
},
{
label: "收益率",
key: "quotation",
colSpan: 1,
rowSpan: 1,
slot: "quotation",
disabled: this.disabled,
type: "durationNumber",
},
{
label: "指令有效期",
key: "insDate",
colSpan: 2,
rowSpan: 1,
type: "date",
slot: "insDate",
required: true,
},
{
label: "清算速度",
key: "clearMethod",
colSpan: 1,
rowSpan: 1,
type: "select",
required: false,
disabled: this.disabled,
options: [{ label: "不限", value: "不限" }, ...this.fuzzyBuyOptions.DEALCLEARMETHODDEAL],
},
{
label: "指定交易员",
key: "fixincomeTrader",
colSpan: 1,
rowSpan: 1,
type: "select",
clearable: true,
required: false,
disabled: this.disabled,
options: [...this.fuzzyBuyOptions.TEADERS],
},
{
label: "备注",
key: "remarks",
colSpan: 3,
rowSpan: 1,
disabled: this.disabled,
type: "input",
},
];
},
},
data() {
return {
initFlagCount: 1,
tradeAmtFlag: false,
insDate: [], //指令有效期
percentageFlag: true,
offerTypeOptions: [
{ label: "净价", value: "cleanPrice" },
{ label: "全价", value: "dirtyPrice" },
{ label: "收益率", value: "yield" },
],
form: {
insType: "卖出", //交易方向
insDate: [], //指令有效期 insDateStart insDateEnd
tradeAmt: "", //券面金额
quotationType: "yield", //报价类型,
quotation: [], //净价 quotationTypeStart quotationTypeEnd
clearMethod: "不限", //清算速度
fixincomeTrader: "", //指定交易员
remarks: "", // 备注
},
};
},
}
</script>
5. 效果