first commit

This commit is contained in:
那些花儿
2025-07-14 08:45:13 +08:00
commit 3e86198a82
2710 changed files with 610330 additions and 0 deletions
@@ -0,0 +1,179 @@
<template>
<div class="guava-main-content">
<div v-show="v === 'index'" key="index" class="transition-item">
<slot></slot>
</div>
<el-card v-if="v === 'edit'" key="edit" shadow="never" class="animated-card">
<template #header>
<el-row type="flex">
<el-col :span="12">
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</el-col>
<el-col :span="12" style="display: flex; justify-content: flex-end">
<slot name="edit_func"></slot>
</el-col>
</el-row>
</template>
<div :class="{ card_scroll: edit_scroll }">
<slot name="edit"></slot>
</div>
</el-card>
<el-card v-if="v === 'view'" key="view" shadow="never" class="animated-card">
<template #header>
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</template>
<div :class="{ card_scroll: !view_page_scroll }">
<slot name="view"></slot>
</div>
</el-card>
<el-card v-if="v === 'approval'" key="approval" shadow="never" class="animated-card">
<template #header>
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</template>
<div :class="{ card_scroll: approval_scroll }">
<slot name="approval"></slot>
</div>
</el-card>
<el-card v-if="v === 'public'" key="public" shadow="never" class="animated-card">
<div slot="header" class="clearfix">
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="back()">返回</el-button>
</div>
<div :class="{ card_scroll: public_card_scroll }">
<slot name="public"></slot>
</div>
</el-card>
</div>
</template>
<script>
module.exports = {
props: {
value: {
type: String,
default: "index"
},
//为true的时候页面不会滚动 card-body滚动 edit view 自己来控制
edit_scroll: {
type: Boolean,
default: false
},
view_scroll: {
type: Boolean,
default: true
},
approval_scroll: {
type: Boolean,
default: false
},
//view 页面是否滚动
view_page_scroll: {
type: Boolean,
default: false
},
public_card_scroll: {
type: Boolean,
default: true
}
},
data() {
return {
v: "index"
}
},
watch: {
v(newValue, oldValue) {
this.$emit("vchange", { newValue, oldValue })
this.$emit("input", newValue)
}
},
methods: {
index(callback = () => {}) {
this.v = "index"
this.$nextTick(() => {
callback()
})
},
edit(callback = () => {}) {
this.v = "edit"
this.$nextTick(() => {
callback()
})
},
view(callback = () => {}) {
this.v = "view"
this.$nextTick(() => {
callback()
})
},
approval(callback = () => {}) {
this.v = "approval"
this.$nextTick(() => {
callback()
})
},
public(callback = () => {}) {
this.v = "public"
this.$nextTick(() => {
callback()
})
},
back(callback = () => {}) {
this.v = "index"
this.$nextTick(() => {
callback()
})
}
}
}
</script>
<style>
.guava-main-content {
min-height: 100%;
}
.animated-card {
animation-name: cardAnimation;
animation-duration: 0.5s;
animation-fill-mode: both;
margin-top: 0 !important;
}
@keyframes cardAnimation {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card_scroll {
max-height: calc(100vh - 56px - 40px - 30px - 50px - 34px);
overflow-y: auto;
position: relative;
}
.card_scroll {
overflow: scroll;
}
.card_scroll::-webkit-scrollbar {
display: none;
}
.card_scroll::-webkit-scrollbar-vertical {
display: none;
}
.card_scroll::-webkit-scrollbar-horizontal {
display: none;
}
</style>
@@ -0,0 +1,13 @@
<script>
module.exports = {
name: "BpmApproval"
}
</script>
<template>
<slot name="form"></slot>
<slot name="process"></slot>
<slot name="approval"></slot>
</template>
<style scoped></style>
@@ -0,0 +1,593 @@
<script>
module.exports = {
name: "index",
props: {
group: {
type: Object,
required: true
},
field_options: {
type: Array,
required: true
},
operator_options: {
type: Array,
default: () => [
{ label: "等于", value: "=" },
{ label: "不等于", value: "!=" },
{ label: "大于", value: ">" },
{ label: "小于", value: "<" },
{ label: "大于等于", value: ">=" },
{ label: "小于等于", value: "<=" },
{ label: "包含", value: "LIKE" },
{ label: "为空", value: "IS NULL" },
{ label: "不为空", value: "IS NOT NULL" },
{ label: "在列表中", value: "IN" },
{ label: "不在列表中", value: "NOT IN" },
{ label: "在范围内", value: "BETWEEN" }
]
},
can_remove: {
type: Boolean,
default: false
}
},
data() {
return {
fieldTypes: {}, // 存储字段类型信息
errors: [] // 存储验证错误
}
},
created() {
// 初始化字段类型信息
this.initFieldTypes()
},
methods: {
initFieldTypes() {
// 从field_options中提取字段类型信息
this.field_options.forEach((field) => {
if (field.type) {
this.fieldTypes[field.value] = field.type
} else {
// 默认为字符串类型
this.fieldTypes[field.value] = "string"
}
})
},
getFieldType(fieldName) {
return this.fieldTypes[fieldName] || "string"
},
addCondition() {
if (!this.group.conditions) {
this.$set(this.group, "conditions", [])
}
this.group.conditions.push({
field: "",
operator: "=",
value: "",
valid: false
})
this.validateAndEmitChange()
},
removeCondition(index) {
this.group.conditions.splice(index, 1)
this.validateAndEmitChange()
},
addGroup() {
if (!this.group.groups) {
this.$set(this.group, "groups", [])
}
this.group.groups.push({
logic: "AND",
conditions: [],
groups: []
})
this.validateAndEmitChange()
},
removeGroup(index) {
this.group.groups.splice(index, 1)
this.validateAndEmitChange()
},
isNullOperator(operator) {
return operator === "IS NULL" || operator === "IS NOT NULL"
},
isInOperator(operator) {
return operator === "IN" || operator === "NOT IN"
},
isBetweenOperator(operator) {
return operator === "BETWEEN"
},
validateCondition(condition) {
// 检查条件是否有效
if (!condition.field || !condition.operator) {
return false
}
// 对于IS NULL和IS NOT NULL操作符,不需要值
if (this.isNullOperator(condition.operator)) {
return true
}
// 对于其他操作符,需要值
return condition.value !== undefined && condition.value !== ""
},
validateGroup(group) {
let isValid = true
// 验证条件
if (group.conditions && group.conditions.length > 0) {
group.conditions.forEach((condition) => {
condition.valid = this.validateCondition(condition)
isValid = isValid && condition.valid
})
}
// 验证嵌套组
if (group.groups && group.groups.length > 0) {
group.groups.forEach((nestedGroup) => {
isValid = isValid && this.validateGroup(nestedGroup)
})
}
// 一个组至少需要一个条件或嵌套组才有效
if ((group.conditions && group.conditions.length > 0) || (group.groups && group.groups.length > 0)) {
return isValid
} else {
return false
}
},
formatValue(value, type) {
// 根据字段类型格式化值
if (value === null || value === undefined || value === "") {
return "NULL"
}
switch (type.toLowerCase()) {
case "number":
case "int":
case "integer":
case "float":
case "double":
case "decimal":
return value
case "date":
case "datetime":
case "time":
case "string":
default:
// 转义单引号
const escaped = String(value).replace(/'/g, "''")
return `'${escaped}'`
}
},
buildSql(group = this.group) {
let sql = []
// 处理条件
if (group.conditions && group.conditions.length > 0) {
group.conditions.forEach((condition) => {
if (condition.field && condition.operator) {
const fieldType = this.getFieldType(condition.field)
if (this.isNullOperator(condition.operator)) {
sql.push(`${condition.field} ${condition.operator}`)
} else if (this.isInOperator(condition.operator) && condition.value) {
// 处理IN操作符 - 假设值是以逗号分隔的列表
const values = condition.value
.split(",")
.map((v) => v.trim())
.filter((v) => v !== "")
.map((v) => this.formatValue(v, fieldType))
.join(", ")
if (values) {
sql.push(`${condition.field} ${condition.operator} (${values})`)
}
} else if (this.isBetweenOperator(condition.operator) && condition.value) {
// 处理BETWEEN操作符 - 假设值是以逗号分隔的两个值
const parts = condition.value.split(",").map((v) => v.trim())
if (parts.length === 2) {
const from = this.formatValue(parts[0], fieldType)
const to = this.formatValue(parts[1], fieldType)
sql.push(`${condition.field} BETWEEN ${from} AND ${to}`)
}
} else if (condition.value !== undefined && condition.value !== "") {
// 其他操作符
const formattedValue = this.formatValue(condition.value, fieldType)
sql.push(`${condition.field} ${condition.operator} ${formattedValue}`)
}
}
})
}
// 处理嵌套组
if (group.groups && group.groups.length > 0) {
group.groups.forEach((nestedGroup) => {
const nestedSql = this.buildSql(nestedGroup)
if (nestedSql) {
sql.push(`(${nestedSql})`)
}
})
}
// 用逻辑运算符连接所有条件
return sql.length > 0 ? sql.join(` ${group.logic} `) : ""
},
validateAndEmitChange() {
// 验证所有条件
const isValid = this.validateGroup(this.group)
// 发出变更事件
this.$emit("change", {
sql: this.buildSql(),
valid: isValid
})
},
getOperatorsByFieldType(fieldType) {
// 根据字段类型过滤可用的操作符
if (!fieldType) return this.operator_options
const type = fieldType.toLowerCase()
return this.operator_options.filter((op) => {
// 数值类型
if (['number', 'int', 'integer', 'float', 'double', 'decimal'].includes(type)) {
// 数值类型不应该使用LIKE操作符
return op.value !== 'LIKE'
}
// 日期类型
else if (['date', 'datetime', 'time'].includes(type)) {
// 日期类型不应该使用LIKE和IN操作符
return !['LIKE', 'IN', 'NOT IN'].includes(op.value)
}
// 字符串类型,支持所有操作符
return true
})
}
},
watch: {
group: {
handler: "validateAndEmitChange",
deep: true
},
field_options: {
handler: "initFieldTypes",
immediate: true
}
}
}
</script>
<template>
<div class="condition-builder">
<!-- 主逻辑选择和组操作 -->
<div class="condition-group-header">
<div class="logic-selector">
<span class="logic-label">匹配方式</span>
<el-radio-group v-model="group.logic" size="small" @change="validateAndEmitChange">
<el-radio-button label="AND">满足所有条件</el-radio-button>
<el-radio-button label="OR">满足任意条件</el-radio-button>
</el-radio-group>
</div>
<el-button v-if="can_remove" type="danger" size="mini" icon="el-icon-delete" @click="$emit('remove')" class="remove-group-btn">
删除组
</el-button>
</div>
<!-- 条件列表 -->
<div class="conditions-container">
<div
v-for="(condition, index) in group.conditions"
:key="'c-' + index"
class="condition-row"
:class="{ 'invalid-condition': condition.valid === false }"
>
<div class="condition-index">{{ index + 1 }}</div>
<el-select
v-model="condition.field"
filterable
placeholder="请选择字段"
size="small"
class="field-select"
@change="validateAndEmitChange"
>
<el-option v-for="field in field_options" :key="field.value" :label="field.label" :value="field.value">
<span>{{ field.label }}</span>
<span class="field-type-hint" v-if="field.type">({{ field.type }})</span>
</el-option>
</el-select>
<el-select v-model="condition.operator" placeholder="操作符" size="small" class="operator-select" @change="validateAndEmitChange">
<el-option
v-for="op in getOperatorsByFieldType(getFieldType(condition.field))"
:key="op.value"
:label="op.label"
:value="op.value"
></el-option>
</el-select>
<!-- 根据操作符类型和字段类型显示不同的输入控件 -->
<template v-if="!isNullOperator(condition.operator)">
<!-- 对于IN操作符显示标签输入框 -->
<el-input
v-if="isInOperator(condition.operator)"
v-model="condition.value"
placeholder="多个值用逗号分隔"
size="small"
class="value-input"
@change="validateAndEmitChange"
></el-input>
<!-- 对于BETWEEN操作符显示两个输入框 -->
<div v-else-if="isBetweenOperator(condition.operator)" class="between-inputs">
<el-input
v-model="condition.value"
placeholder="起始值,结束值"
size="small"
class="value-input"
@change="validateAndEmitChange"
></el-input>
</div>
<!-- 对于其他操作符根据字段类型显示不同的输入控件 -->
<template v-else>
<!-- 数字类型 -->
<el-input
v-if="['number', 'int', 'integer', 'float', 'double', 'decimal'].includes(getFieldType(condition.field))"
v-model.number="condition.value"
placeholder="请输入数值"
size="small"
class="value-input"
type="number"
@change="validateAndEmitChange"
></el-input>
<!-- 日期类型 -->
<el-date-picker
v-else-if="getFieldType(condition.field) === 'date'"
v-model="condition.value"
type="date"
placeholder="选择日期"
size="small"
class="value-input"
value-format="yyyy-MM-dd"
@change="validateAndEmitChange"
></el-date-picker>
<!-- 日期时间类型 -->
<el-date-picker
v-else-if="getFieldType(condition.field) === 'datetime'"
v-model="condition.value"
type="datetime"
placeholder="选择日期时间"
size="small"
class="value-input"
value-format="yyyy-MM-dd HH:mm:ss"
@change="validateAndEmitChange"
></el-date-picker>
<!-- 时间类型 -->
<el-time-picker
v-else-if="getFieldType(condition.field) === 'time'"
v-model="condition.value"
placeholder="选择时间"
size="small"
class="value-input"
value-format="HH:mm:ss"
@change="validateAndEmitChange"
></el-time-picker>
<!-- 默认为字符串类型 -->
<el-input
v-else
v-model="condition.value"
placeholder="请输入值"
size="small"
class="value-input"
@change="validateAndEmitChange"
></el-input>
</template>
</template>
<!-- 空值占位使布局保持一致 -->
<div v-else class="value-placeholder"></div>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="removeCondition(index)" class="remove-btn"></el-button>
</div>
<!-- 空条件提示 -->
<div class="empty-condition-hint" v-if="!group.conditions || group.conditions.length === 0">
<i class="el-icon-info"></i>
请添加筛选条件
</div>
</div>
<!-- 嵌套条件组 -->
<div v-for="(nestedGroup, index) in group.groups" :key="'g-' + index" class="nested-group">
<div class="nested-group-header">
<span class="nested-group-title">子条件组 {{ index + 1 }}</span>
</div>
<condition-group
:group="nestedGroup"
:field_options="field_options"
:operator_options="operator_options"
:can_remove="true"
@remove="removeGroup(index)"
@change="validateAndEmitChange"
></condition-group>
</div>
<!-- 操作按钮区 -->
<div class="condition-actions">
<el-button type="primary" size="small" icon="el-icon-plus" @click="addCondition">添加条件</el-button>
<el-button type="success" size="small" icon="el-icon-folder-add" @click="addGroup">添加条件组</el-button>
</div>
<!-- SQL预览 -->
<div class="sql-preview" v-if="buildSql()">
<div class="sql-preview-header">
<div class="sql-preview-title">条件预览:</div>
</div>
<el-input type="textarea" :value="buildSql()" readonly :rows="2" class="sql-preview-content"></el-input>
</div>
</div>
</template>
<style scoped>
.condition-builder {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 15px;
margin-bottom: 15px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.condition-group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.logic-label {
margin-right: 10px;
color: #606266;
}
.conditions-container {
padding: 5px;
margin-bottom: 15px;
}
.condition-row {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 8px;
background-color: #f9f9f9;
border-radius: 4px;
transition: all 0.3s;
}
.condition-row:hover {
background-color: #f0f7ff;
}
.invalid-condition {
border: 1px dashed #f56c6c;
}
.condition-index {
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #409eff;
color: white;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
}
.field-select {
width: 150px;
margin-right: 10px;
}
.operator-select {
width: 120px;
margin-right: 10px;
}
.value-input {
width: 200px;
margin-right: 10px;
}
.between-inputs {
display: flex;
align-items: center;
width: 200px;
margin-right: 10px;
}
.value-placeholder {
width: 200px;
margin-right: 10px;
}
.field-type-hint {
color: #909399;
margin-left: 5px;
font-size: 12px;
}
.nested-group {
margin: 10px 0;
padding: 10px 0 10px 20px;
border-left: 2px solid #409eff;
background-color: #f9fafc;
border-radius: 0 4px 4px 0;
}
.nested-group-header {
margin-bottom: 10px;
}
.nested-group-title {
font-weight: bold;
color: #409eff;
}
.condition-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #ebeef5;
}
.remove-btn {
margin-left: auto;
}
.remove-group-btn {
margin-left: 10px;
}
.sql-preview {
margin-top: 20px;
padding: 15px;
background-color: #f8f8f8;
border-radius: 4px;
border-left: 3px solid #409eff;
}
.sql-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.sql-preview-title {
font-size: 14px;
font-weight: bold;
color: #303133;
}
.sql-preview-content {
font-family: "Courier New", Courier, monospace;
background-color: #f8f8f8;
}
.empty-condition-hint {
padding: 15px;
text-align: center;
color: #909399;
background-color: #f5f7fa;
border-radius: 4px;
margin: 10px 0;
}
</style>
@@ -0,0 +1,73 @@
<template>
<div>
<el-button type="primary" @click="onOpen" size="small">打开设计</el-button>
<el-dialog title="表单设计" :visible.sync="dialogVisible" append-to-body width="80%">
<fc-designer ref="designerRef" height="100vh" :config="{ showSaveBtn: true }" @save="onSave"></fc-designer>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="onConfirm"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
module.exports = {
name: "index",
props: {
value: {
type: [Object, String],
required: false
}
},
data() {
return {
dialogVisible: false,
form: {}
}
},
watch: {
value: {
handler(newVal) {
if (newVal && typeof newVal === "object") {
this.form = newVal
} else if (newVal && typeof newVal === "string") {
try {
this.form = JSON.parse(newVal)
} catch (error) {
this.form = {}
}
}
console.log(this.form)
},
deep: true,
immediate: true
}
},
methods: {
onOpen() {
this.dialogVisible = true
this.$nextTick(() => {
console.log(this.value)
console.log(this.form)
console.log(this.$refs.designerRef)
if (this.$refs.designerRef && this.form) {
this.$refs.designerRef.setOptions(this.form.options)
this.$refs.designerRef.setRule(this.form.rule)
}
})
},
onSave(val) {
console.log(val)
this.form = val
},
onConfirm() {
console.log(this.$refs.designerRef)
this.$emit("input", this.form)
this.dialogVisible = false
}
}
}
</script>
<style scoped></style>
@@ -0,0 +1,65 @@
<template>
<div>
{{ formKey }}
<el-button type="primary" v-for="item in buttons" :key="item.name" @click="buttonClick(item)" size="small">
{{ item.name }}
</el-button>
</div>
</template>
<script>
module.exports = {
name: "index",
props: {
taskId: {
type: Number,
required: true
},
formKey: {
type: String,
required: true
}
},
data() {
return {
buttons: []
}
},
methods: {
getButtons() {
$.get("/platform/flow/common/loadSequenceFlows", { taskId: this.taskId }).then((res) => {
if (res.code === 0) {
this.buttons = res.data
}
})
},
buttonClick(item) {
this.$root.$refs[this.formKey].validate((valid) => {
if (valid) {
this.$confirm(`您确定要${item.name}吗,${item.name}后将流转到${item.toNode},是否继续?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
})
.then(() => {
this.$emit("confirm", item.name)
})
.catch(() => {})
}
})
}
},
watch: {
taskId: {
handler: function (val) {
if (val) {
this.getButtons()
}
},
immediate: true
}
}
}
</script>
<style scoped></style>
@@ -0,0 +1,242 @@
<template>
<div>
<!-- :is="mode === 'dialog' ? 'el-dialog' : 'div'"-->
<!-- title="详情" v-bind="$attrs"-->
<div v-if="visible">
<el-tabs v-model="tabActive">
<el-tab-pane name="info" label="基本信息">
<slot name="businessInfo"></slot>
</el-tab-pane>
<el-tab-pane name="processRecords" label="审批记录">
<slot name="processRecords"></slot>
</el-tab-pane>
<el-tab-pane name="approvalChart" label="流程跟踪">
<snaker-flow-designer
style="height: 600px"
v-model="flowData"
v-if="visible"
:show-doc="false"
:viewer="true"
nodeRenderType="html"
:wfConfig="{
showHelp: false
}"
:high-light="highLight"
></snaker-flow-designer>
</el-tab-pane>
</el-tabs>
<el-row
class="audit-wrap"
v-if="visible && isAudit && record && record.processTaskState === $processConstant.TASK_STATE.DOING && tabActive !== 'approvalChart'"
>
<div class="left-span-label" style="margin-top: 20px; margin-bottom: 20px">
{{ record.processTaskDisplayName }}
</div>
<slot name="form"></slot>
<el-row type="flex" justify="end" class="flow-button-wrap">
<el-button
@click="
visible = false
$emit('close')
"
size="small"
>
取消
</el-button>
<button
v-for="item in buttons"
:key="item.type"
class="task-button"
:style="buttonStyle(item)"
@click="buttonClick(item)"
@mouseenter="buttonMouseEnter($event, item)"
@mouseleave="buttonMouseLeave($event, item)"
>
{{ item.replaceText || item.text }}
</button>
</el-row>
</el-row>
</div>
</div>
</template>
<script>
module.exports = {
name: "flowAuditIndex",
props: {
form_valid_success: {
type: Boolean,
default: false
}
},
watch: {
//监听父组件的表单是否验证成功
form_valid_success(val) {
if (val) {
this.buttonClick(this.currentClickButton)
}
}
},
data() {
return {
visible: false,
tabActive: "info",
//是否需要审核
isAudit: false,
// 回退列表数据
returnTaskList: [],
//回退弹窗
returnOpen: false,
//任务记录
record: {},
//审核按钮列表
buttons: [],
//任务表单
taskForm: {},
//当前点击的按钮
currentClickButton: {},
//流程数据
flowData: {},
//高亮数据
highLight: {}
}
},
methods: {
buttonStyle(item) {
return {
background: item.color
}
},
buttonMouseEnter($event, item) {
console.log($event)
console.log(item)
},
buttonMouseLeave() {},
onOpen(record, isAudit = false) {
this.isAudit = isAudit
this.record = record
this.getApprovalChart(record.processInstId)
if (isAudit) {
this.getButtons()
}
},
//关闭
onClose() {
this.visible = false
},
//流程图数据
getApprovalChart(id) {
$.get("/platform/wf/processCommon/processJsonHighLight", { id }).then((res) => {
if (res.code === 0) {
this.flowData = res.data.instJson
this.highLight = res.data.highLight
this.visible = true
}
})
},
getButtons() {
const { processTaskName, processDefineId } = this.record
$.get("/platform/wf/processCommon/listButtonByTaskName", {
defineId: processDefineId,
taskName: processTaskName
}).then((res) => {
if (res.code === 0) {
this.buttons = res.data
}
})
},
buttonClick(item) {
this.currentClickButton = item
this.$emit("form_valid", item.type)
if (!this.form_valid_success) return
//指定下一节点审批人 只有在同意的情况下
// if (!["AGREE"].includes(item.type)) {
// }
// if (item.type === "JUMP") {
// return
// }
if (this.form_valid_success) {
this.submitTask()
}
},
submitReturn() {
this.$refs.taskFormRef.validate((valid) => {
if (valid) {
this.returnOpen = false
this.submitTask()
}
})
},
submitTask() {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
})
.then(() => {
this.taskForm.processTaskId = this.record.processTaskId
this.taskForm.processInstId = this.record.processInstId
this.taskForm.submitType = this.currentClickButton.code
this.$emit("form_confirm", this.taskForm)
})
.catch(() => {})
}
}
}
</script>
<style scoped>
.audit-wrap {
margin-top: 20px;
border-top: 1px solid var(--border-color-lighter);
padding-top: 20px;
}
.flow-button-wrap {
margin-top: 20px;
border-top: 1px solid var(--border-color-lighter);
padding-top: 20px;
}
.el-tabs__content {
margin-top: 20px;
}
.el-button + .task-button,
.task-button + .task-button {
margin-left: 10px;
}
.task-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
/*background: var(--button-default-background-color);*/
/*border: 1px solid var(--border-color-base);*/
border: none;
color: var(--color-white);
-webkit-appearance: none;
text-align: center;
-webkit-box-sizing: border-box;
box-sizing: border-box;
outline: none;
margin: 0;
-webkit-transition: 0.1s;
transition: 0.1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
padding: 9px 15px;
font-size: 12px;
border-radius: 3px;
}
</style>
@@ -0,0 +1,240 @@
<template>
<div>
<div v-if="visible">
<van-tabs v-model="tabActive">
<van-tab name="info" title="基本信息">
<slot name="businessInfo"></slot>
</van-tab>
<van-tab name="processRecords" title="审批记录">
<slot name="processRecords"></slot>
</van-tab>
<van-tab name="approvalChart" title="流程跟踪">
<snaker-flow-designer
style="height: 600px"
v-model="flowData"
v-if="visible"
:show-doc="false"
:viewer="true"
nodeRenderType="html"
:wfConfig="{
showHelp: false
}"
:high-light="highLight"
></snaker-flow-designer>
</van-tab>
</van-tabs>
<van-row
class="audit-wrap"
v-if="visible && isAudit && record && record.processTaskState === $processConstant.TASK_STATE.DOING && tabActive !== 'approvalChart'"
>
<div class="left-span-label" style="margin-top: 10px; margin-bottom: 10px">
<van-text-box type="4" style="padding: 0 10px 0" :title="record.processTaskDisplayName"></van-text-box>
</div>
<slot name="form"></slot>
<van-row type="flex" justify="end" class="flow-button-wrap">
<van-button
style="margin-right: 5px"
@click="
visible = false
$emit('close')
"
size="middle"
>
取消
</van-button>
<button
v-for="item in buttons"
:key="item.type"
class="task-button"
:style="buttonStyle(item)"
@click="buttonClick(item)"
@mouseenter="buttonMouseEnter($event, item)"
@mouseleave="buttonMouseLeave($event, item)"
>
{{ item.replaceText || item.text }}
</button>
</van-row>
</van-row>
</div>
</div>
</template>
<script>
module.exports = {
name: "flowH5AuditIndex",
props: {
form_valid_success: {
type: Boolean,
default: false
}
},
watch: {
//监听父组件的表单是否验证成功
form_valid_success(val) {
if (val) {
this.buttonClick(this.currentClickButton)
}
}
},
data() {
return {
visible: false,
tabActive: "info",
//是否需要审核
isAudit: false,
// 回退列表数据
returnTaskList: [],
//回退弹窗
returnOpen: false,
//任务记录
record: {},
//审核按钮列表
buttons: [],
//任务表单
taskForm: {},
//当前点击的按钮
currentClickButton: {},
//流程数据
flowData: {},
//高亮数据
highLight: {}
}
},
methods: {
buttonStyle(item) {
return {
background: item.color
}
},
buttonMouseEnter($event, item) {
console.log($event)
console.log(item)
},
buttonMouseLeave() {},
onOpen(record, isAudit = false) {
this.isAudit = isAudit
this.record = record
this.getApprovalChart(record.processInstId)
if (isAudit) {
this.getButtons()
}
},
//关闭
onClose() {
this.visible = false
},
//流程图数据
getApprovalChart(id) {
$.get("/platform/wf/processCommon/processJsonHighLight", { id }).then((res) => {
if (res.code === 0) {
this.flowData = res.data.instJson
this.highLight = res.data.highLight
this.visible = true
}
})
},
getButtons() {
const { processTaskName, processDefineId } = this.record
$.get("/platform/wf/processCommon/listButtonByTaskName", {
defineId: processDefineId,
taskName: processTaskName
}).then((res) => {
if (res.code === 0) {
this.buttons = res.data
}
})
},
buttonClick(item) {
this.currentClickButton = item
this.$emit("form_valid", item.type)
if (!this.form_valid_success) return
//指定下一节点审批人 只有在同意的情况下
// if (!["AGREE"].includes(item.type)) {
// }
// if (item.type === "JUMP") {
// return
// }
if (this.form_valid_success) {
this.submitTask()
}
},
submitReturn() {
this.$refs.taskFormRef.validate((valid) => {
if (valid) {
this.returnOpen = false
this.submitTask()
}
})
},
submitTask() {
this.$dialog
.confirm({
title: "提示",
message: "您确定要提交吗?"
})
.then(() => {
this.taskForm.processTaskId = this.record.processTaskId
this.taskForm.processInstId = this.record.processInstId
this.taskForm.submitType = this.currentClickButton.code
this.$emit("form_confirm", this.taskForm)
})
.catch(() => {})
}
}
}
</script>
<style scoped>
.audit-wrap {
margin-top: 20px;
border-top: 1px solid var(--border-color-lighter);
padding-top: 20px;
}
.flow-button-wrap {
margin-top: 20px;
border-top: 1px solid var(--border-color-lighter);
padding-top: 20px;
margin-right: 5px;
}
.el-tabs__content {
margin-top: 20px;
}
.el-button + .task-button,
.task-button + .task-button {
margin-left: 10px;
}
.task-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
/*background: var(--button-default-background-color);*/
/*border: 1px solid var(--border-color-base);*/
border: none;
color: var(--color-white);
-webkit-appearance: none;
text-align: center;
-webkit-box-sizing: border-box;
box-sizing: border-box;
outline: none;
margin: 0;
-webkit-transition: 0.1s;
transition: 0.1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
padding: 9px 15px;
font-size: 12px;
border-radius: 3px;
}
</style>
@@ -0,0 +1,57 @@
<template>
<div>
<el-card v-if="!visible">
<div></div>
2222222222222222222222222222222222222222 2222222222222222222222222222222222222222 2222222222222222222222222222222222222222
2222222222222222222222222222222222222222 申请需要的说明 流程图等等等等等等等等等等
<el-row :gutter="10">
<el-button type="primary" @click="onStart">开始申请</el-button>
</el-row>
</el-card>
<template v-if="visible">
<slot></slot>
</template>
</div>
</template>
<script>
module.exports = {
name: "index",
props: {},
data() {
return {
visible: false,
taskId: parseInt(GetQueryString("TASK_ID"))
}
},
methods: {
onStart() {
const loading = this.$loading({
lock: true,
text: "Loading",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)"
})
$.post(loc() + "/startProcess").then((res) => {
if (res.code === 0) {
this.$emit("task_id", res.data.taskId)
this.$emit("business_id", res.data.businessId)
setTimeout(() => {
loading.close()
this.visible = true
}, 1000)
}
})
}
},
created() {
if (this.taskId) {
this.visible = true
}
}
}
</script>
<style scoped></style>
@@ -0,0 +1,167 @@
<template>
<div style="position: relative !important">
<div style="margin-bottom: 10px">
<el-autocomplete
v-model="addressValue"
style="width: 100%"
:fetch-suggestions="querySearchAsync"
placeholder="请输入关键词查询地址"
@select="handleSelect"
/>
</div>
<div id="mapContainer"></div>
</div>
</template>
<script>
let marker = null
let circle = null
module.exports = {
name: "mapContainer",
props: {
isCircle: {
type: Boolean,
default: false
},
radius: {
type: Number,
default: 50
},
position: {
type: Array,
default: () => {
return []
}
},
view: { type: Boolean, default: false }
},
data() {
return {
// 此处不声明 map 对象,可以直接使用 this.map赋值或者采用非响应式的普通对象来存储。
map: null,
poi: this.position,
appMapCenterPointX: 0,
appMapCenterPointY: 0,
addressValue: "",
placeSearchComponent: null
}
},
methods: {
handleSelect(item) {
if (item.location) {
// 定位到中心点
this.map.setCenter(item.location)
// TODO 获取数据,对数据进行操作如:添加marker等
this.clearMarker()
this.createMarker(item.location)
this.poi = [item.location.lng, item.location.lat]
}
},
querySearchAsync(queryString, cb) {
if (!queryString) {
return
}
this.placeSearchComponent.search(queryString, function (status, result) {
// 查询成功时,result即对应匹配的POI信息
if (status !== "error") {
result.poiList.pois.forEach((item) => {
item.value = item.name + "【详细地址:" + item.address + "】"
})
cb(result.poiList.pois)
}
})
},
initMap() {
this.map = new AMap.Map("mapContainer", {
// 设置地图容器id
resizeEnable: true,
zoom: 16, // 初始化地图级别
center: [this.appMapCenterPointX, this.appMapCenterPointY] // 初始化地图中心点位置
})
console.log(this.map)
this.placeSearchComponent = new AMap.PlaceSearch({
// city 指定搜索所在城市,支持传入格式有:城市名、citycode和adcode
city: ""
})
this.clearMarker()
if (this.poi && this.poi.length > 0) {
const coordinateArray = this.poi
this.createMarker(coordinateArray)
} else {
this.createMarker([this.appMapCenterPointX, this.appMapCenterPointY])
}
this.map.on("click", (e) => {
if (!this.view) {
this.clearMarker()
const position = [e.lnglat.getLng(), e.lnglat.getLat()]
this.createMarker(position)
this.poi = position
}
})
},
// 清楚签到点位
clearMarker() {
if (marker) {
this.map.remove(marker)
}
if (circle && this.isCircle) {
this.map.remove(circle)
}
},
// 创建签到点位
createMarker(position) {
if (this.isCircle) {
circle = new AMap.Circle({
center: new AMap.LngLat(position[0], position[1]), // 圆心位置
radius: this.radius, // 半径
strokeColor: "#F33", // 线颜色
strokeOpacity: 1, // 线透明度
strokeWeight: 1, // 线粗细度
fillColor: "#ee2200", // 填充颜色
fillOpacity: 0.35 // 填充透明度
})
this.map.add(circle)
this.getAddress(position)
this.map.setFitView()
}
marker = new AMap.Marker({
position: position,
offset: new AMap.Pixel(0, 0)
})
this.map.add(marker)
this.getAddress(position)
this.map.setFitView(null, false, [150, 60, 100, 60])
},
getAddress() {},
async getConfigKey(key) {
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
return resp.data
}
},
watch: {
position(newVal) {
this.poi = newVal
},
poi(newVal) {
this.$emit("update:position", newVal)
}
},
async mounted() {
// DOM初始化完成进行地图初始化
this.$nextTick(async () => {
this.appMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
this.appMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
this.initMap()
})
}
}
</script>
<style scoped>
#mapContainer {
padding: 0;
margin: 0;
width: 100%;
height: 500px;
}
</style>
@@ -0,0 +1,349 @@
<!--<template>-->
<!-- <div>-->
<!-- <div class="ant-spin-nested-loading" v-if="spinning && $slots.default">-->
<!-- <div class="ant-spin ant-spin-spinning" :class="{ 'ant-spin-lg': size === 'large', 'ant-spin-small': size === 'small' }">-->
<!-- <span class="ant-spin-dot ant-spin-dot-spin">-->
<!-- <i class="ant-spin-dot-item"></i>-->
<!-- <i class="ant-spin-dot-item"></i>-->
<!-- <i class="ant-spin-dot-item"></i>-->
<!-- <i class="ant-spin-dot-item"></i>-->
<!-- </span>-->
<!-- <div class="ant-spin-text" v-if="$props.tip">-->
<!-- {{ tip }}-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="ant-spin-container" :class="{ 'ant-spin-blur': spinning }">-->
<!-- <slot></slot>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="ant-spin ant-spin-spinning" :class="{ 'ant-spin-lg': size === 'large', 'ant-spin-small': size === 'small' }" v-else>-->
<!-- <span class="ant-spin-dot ant-spin-dot-spin">-->
<!-- <i class="ant-spin-dot-item"></i>-->
<!-- <i class="ant-spin-dot-item"></i>-->
<!-- <i class="ant-spin-dot-item"></i>-->
<!-- <i class="ant-spin-dot-item"></i>-->
<!-- </span>-->
<!-- <div class="ant-spin-text" v-if="$props.tip">-->
<!-- {{ tip }}-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</template>-->
<!--<script>-->
<!--module.exports = {-->
<!-- name: "index",-->
<!-- props: {-->
<!-- size: String,-->
<!-- tip: String,-->
<!-- spinning: Boolean-->
<!-- },-->
<!-- computed: {}-->
<!--}-->
<!--</script>-->
<template>
<!-- <div :class="spinClassName" :style="style">-->
<!-- <div v-if="renderIndicator" class="ant-spin-dot">-->
<!-- <span class="ant-spin-dot-item"></span>-->
<!-- <span class="ant-spin-dot-item"></span>-->
<!-- <span class="ant-spin-dot-item"></span>-->
<!-- <span class="ant-spin-dot-item"></span>-->
<!-- </div>-->
<!-- <div v-if="tip" class="ant-spin-text">{{ tip }}</div>-->
<!-- <slot></slot>-->
<!-- </div>-->
<div class="ant-spin-nested-loading" data-v-2f42c11b="">
<div>
<div class="ant-spin ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin">
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
</span>
<!---->
</div>
</div>
<div class="ant-spin-container ant-spin-blur">
<el-alert>
<p>22222222222222</p>
<p>22222222222222</p>
<p>22222222222222</p>
<p>22222222222222</p>
<p>22222222222222</p>
</el-alert>
</div>
</div>
</template>
<script>
module.exports = {
name: "EL_SPIN",
props: {
spinning: {
type: Boolean,
default: undefined
},
size: String,
wrapperClassName: String,
tip: String,
delay: Number,
indicator: String
},
computed: {
spinClassName() {
const { size, spinning } = this
return {
[`ant-spin`]: true,
[`ant-spin-sm`]: size === "small",
[`ant-spin-lg`]: size === "large",
[`ant-spin-spinning`]: spinning,
[`ant-spin-show-text`]: !!this.tip,
[`ant-spin-rtl`]: this.direction === "rtl"
}
},
renderIndicator() {
// const indicator = getComponent(this, "indicator")
// return indicator !== null && !Array.isArray(indicator)
return true
},
style() {
return this.$attrs.style
},
direction() {
// return this.configProvider.direction
}
}
}
</script>
<style scoped>
.ant-spin {
box-sizing: border-box;
margin: 0;
padding: 0;
color: #000000d9;
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5715;
list-style: none;
font-feature-settings: tnum;
position: absolute;
display: none;
color: #1890ff;
text-align: center;
vertical-align: middle;
opacity: 0;
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-nested-loading {
position: relative;
}
.ant-spin-nested-loading > div > .ant-spin {
position: absolute;
top: 0;
left: 0;
z-index: 4;
display: block;
width: 100%;
height: 100%;
max-height: 400px;
}
.ant-spin-nested-loading > div > .ant-spin .ant-spin-dot {
position: absolute;
top: 50%;
left: 50%;
margin: -10px;
}
.ant-spin-nested-loading > div > .ant-spin .ant-spin-text {
position: absolute;
top: 50%;
width: 100%;
padding-top: 5px;
text-shadow: 0 1px 2px #fff;
}
.ant-spin-nested-loading > div > .ant-spin.ant-spin-show-text .ant-spin-dot {
margin-top: -20px;
}
.ant-spin-nested-loading > div > .ant-spin-sm .ant-spin-dot {
margin: -7px;
}
.ant-spin-nested-loading > div > .ant-spin-sm .ant-spin-text {
padding-top: 2px;
}
.ant-spin-nested-loading > div > .ant-spin-sm.ant-spin-show-text .ant-spin-dot {
margin-top: -17px;
}
.ant-spin-nested-loading > div > .ant-spin-lg .ant-spin-dot {
margin: -16px;
}
.ant-spin-nested-loading > div > .ant-spin-lg .ant-spin-text {
padding-top: 11px;
}
.ant-spin-nested-loading > div > .ant-spin-lg.ant-spin-show-text .ant-spin-dot {
margin-top: -26px;
}
.ant-spin-container {
position: relative;
transition: opacity 0.3s;
}
.ant-spin-container:after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
display: none \;
width: 100%;
height: 100%;
background: #fff;
opacity: 0;
transition: all 0.3s;
content: "";
pointer-events: none;
}
.ant-spin-blur {
clear: both;
overflow: hidden;
opacity: 0.5;
user-select: none;
pointer-events: none;
}
.ant-spin-blur:after {
opacity: 0.4;
pointer-events: auto;
}
.ant-spin-tip {
color: #00000073;
}
.ant-spin-dot {
position: relative;
display: inline-block;
font-size: 20px;
width: 1em;
height: 1em;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
transform: scale(0.75);
transform-origin: 50% 50%;
opacity: 0.3;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
transform: rotate(45deg);
animation: antRotate 1.2s infinite linear;
}
.ant-spin-sm .ant-spin-dot {
font-size: 14px;
}
.ant-spin-sm .ant-spin-dot i {
width: 6px;
height: 6px;
}
.ant-spin-lg .ant-spin-dot {
font-size: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
.ant-spin.ant-spin-show-text .ant-spin-text {
display: block;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antRotate {
to {
transform: rotate(405deg);
}
}
.ant-spin-rtl {
direction: rtl;
}
.ant-spin-rtl .ant-spin-dot-spin {
transform: rotate(-45deg);
animation-name: antRotateRtl;
}
@keyframes antRotateRtl {
to {
transform: rotate(-405deg);
}
}
</style>
@@ -0,0 +1,48 @@
class SysDictData {
constructor(dict) {
this.dict = dict
}
async init(dictNames) {
const ps = []
dictNames.forEach((name) => {
Vue.set(this.dict.type, name, null)
ps.push(
$.get("/open/common/dictOptions", { code: name }).then((res) => {
const dictValue = res.data.map((v) => {
return {
name: v.name,
text: v.name,
label: v.name,
code: v.code,
raw: v
}
})
this.dict.type[name] = Object.freeze(dictValue)
})
)
})
await Promise.all(ps)
}
}
window.dictData = {}
window.dictData.install = function (Vue) {
Vue.mixin({
data() {
if (this.$options.dicts instanceof Array && this.$options.dicts.length > 0) {
return { dict: { type: {} } }
} else {
return {}
}
},
created() {
if (this.$options.dicts instanceof Array && this.$options.dicts.length > 0) {
new SysDictData(this.dict).init(this.$options.dicts).then()
}
}
})
}
if (window.Vue) {
Vue.use(window.dictData)
}
@@ -0,0 +1,113 @@
<template>
<el-select
v-model="valueAsString"
:placeholder="placeholder"
@change="onChange"
:disabled="disabled"
:clearable="clearable"
:multiple="multiple"
:filterable="filterable"
:size="size"
style="width: 100%"
v-bind="$attrs"
>
<el-option
v-for="item in options"
v-if="!item.disabled"
:key="item[option_value]"
:label="item[option_label]"
:value="item[option_value]"
></el-option>
</el-select>
</template>
<script>
module.exports = {
props: {
value: { type: String },
code: {
type: [String, Array],
default: ""
},
option_value: {
type: String,
default: "code"
},
option_label: {
type: String,
default: "name"
},
size: {
type: String,
default: ""
},
placeholder: {
type: String,
default: "请选择"
},
clearable: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: false
},
filterable: {
type: Boolean,
default: true
}
},
model: {
prop: "value",
event: "change"
},
data() {
return {
options: []
}
},
computed: {
valueAsString: {
get() {
if (Array.isArray(this.value)) {
return this.value
} else {
return this.value ? this.value.toString() : ""
}
},
set(val) {}
}
},
watch: {
code(val) {
this.flushOptions()
}
},
methods: {
onChange(val) {
this.$emit("change", val)
},
async flushOptions() {
if (!this.code) {
this.options = []
return
}
$.get("/open/common/dictOptions", { code: this.code }).then((res) => {
this.options = res.data
})
}
},
created() {
this.flushOptions()
}
}
</script>
<style></style>
@@ -0,0 +1,45 @@
<template>
<div>
<template v-for="(item, index) in options">
<template v-if="values.includes(item[option_value])">
<span :key="item.value">{{ item[option_label] }}</span>
</template>
</template>
</div>
</template>
<script>
module.exports = {
name: "DictTag",
props: {
options: {
type: Array,
default: null
},
option_value: {
type: String,
default: "code"
},
option_label: {
type: String,
default: "name"
},
value: [Number, String, Array]
},
computed: {
values() {
if (this.value !== null && typeof this.value !== "undefined") {
return Array.isArray(this.value) ? this.value : [String(this.value)]
} else {
return []
}
}
}
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>
@@ -0,0 +1,44 @@
<template>
<el-dialog title="查看" :visible.sync="dialogVisible" width="60%">
<form-create :value.sync="dynamicFormData" v-model="fapi" :rule="formCreateRule" :option="formCreateOption" disabled></form-create>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="dialogVisible = false"> </el-button>
</span>
</el-dialog>
</template>
<script>
module.exports = {
name: "dynamic-Table-form-eval",
data() {
return {
dialogVisible: false,
dynamicFormData: {},
formCreateRule: [],
formCreateOption: {},
fapi: null
}
},
methods: {
onOpen(formConfig, dynamicFormData) {
debugger
this.dialogVisible = true
this.$nextTick(() => {
this.formCreateRule = formCreate.parseJson(formConfig.rule)
this.formCreateOption = formCreate.parseJson(formConfig.options)
if (typeof dynamicFormData === "string") {
try {
this.dynamicFormData = JSON.parse(dynamicFormData)
} catch (e) {}
} else if (typeof dynamicFormData === "object") {
this.dynamicFormData = dynamicFormData
}
this.fapi.disabled(true)
})
}
}
}
</script>
<style scoped></style>
@@ -0,0 +1,169 @@
<template>
<div>
<div class="file-preview-container" v-if="fileList && fileList.length > 0">
<div class="file-item" title="点击查看" v-for="item in fileList" :key="item.id">
<el-image
v-if="item.suffix === 'jpg' || item.suffix === 'png' || item.suffix === 'jpeg' || item.suffix === 'gif'"
:src="item.thumbnail"
:fit="'cover'"
:style="{ width: '100%', height: '100%' }"
@click.stop="$commonUtil.previewFile(item)"
></el-image>
<el-image
v-else
:src="$commonUtil.getFileItemShowIcon(item.suffix)"
:fit="'cover'"
@click.stop="$commonUtil.previewFile(item)"
:style="{ width: '100%', height: '100%' }"
></el-image>
<div class="file-download" title="点击下载" @click="$downLoad(item.downloadPath)">
<i class="el-icon-download"></i>
{{ item.name }}
</div>
</div>
</div>
<el-empty v-else description="暂无" :image-size="60"></el-empty>
</div>
</template>
<script>
module.exports = {
name: "sysFilePreview",
props: {
files: {
type: [String, Array],
default: undefined,
required: false
},
complete_result: {
type: Boolean,
default: false,
required: false
}
},
watch: {
files: {
handler(val) {
if (val) {
this.buildEchoFileObject(val)
} else {
this.fileList = []
}
},
immediate: true
}
},
data() {
return {
fileList: []
}
},
methods: {
buildEchoFileObject(val) {
let ids = []
if (this.complete_result) {
if (Array.isArray(val)) {
ids = val.map((item) => {
if (item.response.data.includes("=")) {
return item?.response?.data.substring(item?.response?.data.lastIndexOf("=") + 1)
} else {
return item?.response?.data
}
})
} else {
try {
ids = JSON.parse(val).map((item) => {
if (item.response.data.includes("=")) {
return item?.response?.data.substring(item?.response?.data.lastIndexOf("=") + 1)
} else {
return item?.response?.data
}
})
} catch (err) {
this.fileList = []
}
}
} else {
//那就是链接 没有直接存id的吧。。
if (Array.isArray(val)) {
ids = val.map((item) => {
return item.substring(item.lastIndexOf("=") + 1)
})
} else {
ids = val.split(",").map((item) => {
return item.substring(item.lastIndexOf("=") + 1)
})
}
}
this.requestFullFile(ids)
},
requestFullFile(ids) {
this.$axios.post("/platform/sys/file/previewFileData", { ids: JSON.stringify(ids) }).then((res) => {
if (res.code === 0) {
this.fileList = res.data
}
})
}
}
}
</script>
<style scoped>
.file-preview-container {
position: relative;
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
column-gap: 10px;
}
.file-preview-container .file-item {
width: 118px;
height: 118px;
position: relative;
transition: all 500ms;
border-radius: 5px;
cursor: pointer;
text-align: center;
column-gap: 15px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.file-preview-container .file-item:hover {
background: rgb(230, 230, 230);
}
.file-preview-container .file-item .el-image {
width: 90px !important;
height: 100%;
transform: translateY(10px);
}
.file-preview-container .file-item .file-download {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 24px;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: white;
background-color: rgb(160, 160, 160);
border-radius: 2px;
opacity: 0.95;
}
.el-empty {
padding: 0;
}
</style>
@@ -0,0 +1,885 @@
/**
* 图标选择器基础数据
* 推荐前往https://icones.js.org下载图标的Vue文件,然后放在src/assets/icons文件夹里面
* 这个网址有118个图标集,包括antd、font awesome、bootstrap、eleme等累计140456个图标
*/
// const uiwIconComponentMap = import.meta.glob("../assets/icons/uiw/*.vue") // 异步方式
//
// const uiwIcons = Object.keys(uiwIconComponentMap).map((key) => {
// return key.slice(key.lastIndexOf("/") + 1, key.lastIndexOf("."))
// })
window.iconSelectConfig = {
icons: [
{
name: "基础",
key: "default",
iconItem: [
{
name: "线框风格",
key: "default",
item: [
"ant-design_account-book-outlined",
"ant-design_aim-outlined",
"ant-design_alert-outlined",
"ant-design_alibaba-outlined",
"ant-design_align-center-outlined",
"ant-design_align-left-outlined",
"ant-design_align-right-outlined",
"ant-design_alipay-circle-outlined",
"ant-design_alipay-outlined",
"ant-design_aliwangwang-outlined",
"ant-design_aliyun-outlined",
"ant-design_amazon-outlined",
"ant-design_android-outlined",
"ant-design_ant-cloud-outlined",
"ant-design_ant-design-outlined",
"ant-design_apartment-outlined",
"ant-design_api-outlined",
"ant-design_apple-outlined",
"ant-design_appstore-add-outlined",
"ant-design_appstore-outlined",
"ant-design_area-chart-outlined",
"ant-design_arrow-down-outlined",
"ant-design_arrow-left-outlined",
"ant-design_arrow-right-outlined",
"ant-design_arrow-up-outlined",
"ant-design_arrows-alt-outlined",
"ant-design_audio-muted-outlined",
"ant-design_audio-outlined",
"ant-design_audit-outlined",
"ant-design_backward-outlined",
"ant-design_baidu-outlined",
"ant-design_bank-outlined",
"ant-design_bar-chart-outlined",
"ant-design_barcode-outlined",
"ant-design_bars-outlined",
"ant-design_behance-outlined",
"ant-design_behance-square-outlined",
"ant-design_bell-outlined",
"ant-design_bg-colors-outlined",
"ant-design_bilibili-outlined",
"ant-design_block-outlined",
"ant-design_bold-outlined",
"ant-design_book-outlined",
"ant-design_border-bottom-outlined",
"ant-design_border-horizontal-outlined",
"ant-design_border-inner-outlined",
"ant-design_border-left-outlined",
"ant-design_border-outer-outlined",
"ant-design_border-outlined",
"ant-design_border-right-outlined",
"ant-design_border-top-outlined",
"ant-design_border-verticle-outlined",
"ant-design_borderless-table-outlined",
"ant-design_box-plot-outlined",
"ant-design_branches-outlined",
"ant-design_bug-outlined",
"ant-design_build-outlined",
"ant-design_bulb-outlined",
"ant-design_calculator-outlined",
"ant-design_calendar-outlined",
"ant-design_camera-outlined",
"ant-design_car-outlined",
"ant-design_caret-down-outlined",
"ant-design_caret-left-outlined",
"ant-design_caret-right-outlined",
"ant-design_caret-up-outlined",
"ant-design_carry-out-outlined",
"ant-design_check-circle-outlined",
"ant-design_check-outlined",
"ant-design_check-square-outlined",
"ant-design_chrome-outlined",
"ant-design_ci-circle-outlined",
"ant-design_ci-outlined",
"ant-design_clear-outlined",
"ant-design_clock-circle-outlined",
"ant-design_close-circle-outlined",
"ant-design_close-outlined",
"ant-design_close-square-outlined",
"ant-design_cloud-download-outlined",
"ant-design_cloud-outlined",
"ant-design_cloud-server-outlined",
"ant-design_cloud-sync-outlined",
"ant-design_cloud-upload-outlined",
"ant-design_cluster-outlined",
"ant-design_code-outlined",
"ant-design_code-sandbox-outlined",
"ant-design_codepen-circle-outlined",
"ant-design_codepen-outlined",
"ant-design_coffee-outlined",
"ant-design_column-height-outlined",
"ant-design_column-width-outlined",
"ant-design_comment-outlined",
"ant-design_compass-outlined",
"ant-design_compress-outlined",
"ant-design_console-sql-outlined",
"ant-design_contacts-outlined",
"ant-design_container-outlined",
"ant-design_control-outlined",
"ant-design_copy-outlined",
"ant-design_copyright-circle-outlined",
"ant-design_copyright-outlined",
"ant-design_credit-card-outlined",
"ant-design_crown-outlined",
"ant-design_customer-service-outlined",
"ant-design_dash-outlined",
"ant-design_dashboard-outlined",
"ant-design_database-outlined",
"ant-design_delete-column-outlined",
"ant-design_delete-outlined",
"ant-design_delete-row-outlined",
"ant-design_delivered-procedure-outlined",
"ant-design_deployment-unit-outlined",
"ant-design_desktop-outlined",
"ant-design_diff-outlined",
"ant-design_dingding-outlined",
"ant-design_dingtalk-outlined",
"ant-design_disconnect-outlined",
"ant-design_discord-outlined",
"ant-design_dislike-outlined",
"ant-design_docker-outlined",
"ant-design_dollar-circle-outlined",
"ant-design_dollar-outlined",
"ant-design_dot-chart-outlined",
"ant-design_dot-net-outlined",
"ant-design_double-left-outlined",
"ant-design_double-right-outlined",
"ant-design_down-circle-outlined",
"ant-design_down-outlined",
"ant-design_down-square-outlined",
"ant-design_download-outlined",
"ant-design_drag-outlined",
"ant-design_dribbble-outlined",
"ant-design_dribbble-square-outlined",
"ant-design_dropbox-outlined",
"ant-design_edit-outlined",
"ant-design_ellipsis-outlined",
"ant-design_enter-outlined",
"ant-design_environment-outlined",
"ant-design_euro-circle-outlined",
"ant-design_euro-outlined",
"ant-design_exception-outlined",
"ant-design_exclamation-circle-outlined",
"ant-design_exclamation-outlined",
"ant-design_expand-alt-outlined",
"ant-design_expand-outlined",
"ant-design_experiment-outlined",
"ant-design_export-outlined",
"ant-design_eye-invisible-outlined",
"ant-design_eye-outlined",
"ant-design_facebook-outlined",
"ant-design_fall-outlined",
"ant-design_fast-backward-outlined",
"ant-design_fast-forward-outlined",
"ant-design_field-binary-outlined",
"ant-design_field-number-outlined",
"ant-design_field-string-outlined",
"ant-design_field-time-outlined",
"ant-design_file-add-outlined",
"ant-design_file-done-outlined",
"ant-design_file-excel-outlined",
"ant-design_file-exclamation-outlined",
"ant-design_file-gif-outlined",
"ant-design_file-image-outlined",
"ant-design_file-jpg-outlined",
"ant-design_file-markdown-outlined",
"ant-design_file-outlined",
"ant-design_file-pdf-outlined",
"ant-design_file-ppt-outlined",
"ant-design_file-protect-outlined",
"ant-design_file-search-outlined",
"ant-design_file-sync-outlined",
"ant-design_file-text-outlined",
"ant-design_file-unknown-outlined",
"ant-design_file-word-outlined",
"ant-design_file-zip-outlined",
"ant-design_filter-outlined",
"ant-design_fire-outlined",
"ant-design_flag-outlined",
"ant-design_folder-add-outlined",
"ant-design_folder-open-outlined",
"ant-design_folder-outlined",
"ant-design_folder-view-outlined",
"ant-design_font-colors-outlined",
"ant-design_font-size-outlined",
"ant-design_fork-outlined",
"ant-design_form-outlined",
"ant-design_format-painter-outlined",
"ant-design_forward-outlined",
"ant-design_frown-outlined",
"ant-design_fullscreen-exit-outlined",
"ant-design_fullscreen-outlined",
"ant-design_function-outlined",
"ant-design_fund-outlined",
"ant-design_fund-projection-screen-outlined",
"ant-design_fund-view-outlined",
"ant-design_funnel-plot-outlined",
"ant-design_gateway-outlined",
"ant-design_gif-outlined",
"ant-design_gift-outlined",
"ant-design_github-outlined",
"ant-design_gitlab-outlined",
"ant-design_global-outlined",
"ant-design_gold-outlined",
"ant-design_google-outlined",
"ant-design_google-plus-outlined",
"ant-design_group-outlined",
"ant-design_harmony-o-s-outlined",
"ant-design_hdd-outlined",
"ant-design_heart-outlined",
"ant-design_heat-map-outlined",
"ant-design_highlight-outlined",
"ant-design_history-outlined",
"ant-design_holder-outlined",
"ant-design_home-outlined",
"ant-design_hourglass-outlined",
"ant-design_html5-outlined",
"ant-design_idcard-outlined",
"ant-design_ie-outlined",
"ant-design_import-outlined",
"ant-design_inbox-outlined",
"ant-design_info-circle-outlined",
"ant-design_info-outlined",
"ant-design_insert-row-above-outlined",
"ant-design_insert-row-below-outlined",
"ant-design_insert-row-left-outlined",
"ant-design_insert-row-right-outlined",
"ant-design_instagram-outlined",
"ant-design_insurance-outlined",
"ant-design_interaction-outlined",
"ant-design_issues-close-outlined",
"ant-design_italic-outlined",
"ant-design_java-outlined",
"ant-design_java-script-outlined",
"ant-design_key-outlined",
"ant-design_kubernetes-outlined",
"ant-design_laptop-outlined",
"ant-design_layout-outlined",
"ant-design_left-circle-outlined",
"ant-design_left-outlined",
"ant-design_left-square-outlined",
"ant-design_like-outlined",
"ant-design_line-chart-outlined",
"ant-design_line-height-outlined",
"ant-design_line-outlined",
"ant-design_link-outlined",
"ant-design_linkedin-outlined",
"ant-design_linux-outlined",
"ant-design_loading-3-quarters-outlined",
"ant-design_loading-outlined",
"ant-design_lock-outlined",
"ant-design_login-outlined",
"ant-design_logout-outlined",
"ant-design_mac-command-outlined",
"ant-design_mail-outlined",
"ant-design_man-outlined",
"ant-design_medicine-box-outlined",
"ant-design_medium-outlined",
"ant-design_medium-workmark-outlined",
"ant-design_meh-outlined",
"ant-design_menu-fold-outlined",
"ant-design_menu-outlined",
"ant-design_menu-unfold-outlined",
"ant-design_merge-cells-outlined",
"ant-design_merge-outlined",
"ant-design_message-outlined",
"ant-design_minus-circle-outlined",
"ant-design_minus-outlined",
"ant-design_minus-square-outlined",
"ant-design_mobile-outlined",
"ant-design_money-collect-outlined",
"ant-design_monitor-outlined",
"ant-design_moon-outlined",
"ant-design_more-outlined",
"ant-design_muted-outlined",
"ant-design_node-collapse-outlined",
"ant-design_node-expand-outlined",
"ant-design_node-index-outlined",
"ant-design_notification-outlined",
"ant-design_number-outlined",
"ant-design_one-to-one-outlined",
"ant-design_open-a-i-outlined",
"ant-design_ordered-list-outlined",
"ant-design_paper-clip-outlined",
"ant-design_partition-outlined",
"ant-design_pause-circle-outlined",
"ant-design_pause-outlined",
"ant-design_pay-circle-outlined",
"ant-design_percentage-outlined",
"ant-design_phone-outlined",
"ant-design_pic-center-outlined",
"ant-design_pic-left-outlined",
"ant-design_pic-right-outlined",
"ant-design_picture-outlined",
"ant-design_pie-chart-outlined",
"ant-design_pinterest-outlined",
"ant-design_play-circle-outlined",
"ant-design_play-square-outlined",
"ant-design_plus-circle-outlined",
"ant-design_plus-outlined",
"ant-design_plus-square-outlined",
"ant-design_pound-circle-outlined",
"ant-design_pound-outlined",
"ant-design_poweroff-outlined",
"ant-design_printer-outlined",
"ant-design_product-outlined",
"ant-design_profile-outlined",
"ant-design_project-outlined",
"ant-design_property-safety-outlined",
"ant-design_pull-request-outlined",
"ant-design_pushpin-outlined",
"ant-design_python-outlined",
"ant-design_qq-outlined",
"ant-design_qrcode-outlined",
"ant-design_question-circle-outlined",
"ant-design_question-outlined",
"ant-design_radar-chart-outlined",
"ant-design_radius-bottomleft-outlined",
"ant-design_radius-bottomright-outlined",
"ant-design_radius-setting-outlined",
"ant-design_radius-upleft-outlined",
"ant-design_radius-upright-outlined",
"ant-design_read-outlined",
"ant-design_reconciliation-outlined",
"ant-design_red-envelope-outlined",
"ant-design_reddit-outlined",
"ant-design_redo-outlined",
"ant-design_reload-outlined",
"ant-design_rest-outlined",
"ant-design_retweet-outlined",
"ant-design_right-circle-outlined",
"ant-design_right-outlined",
"ant-design_right-square-outlined",
"ant-design_rise-outlined",
"ant-design_robot-outlined",
"ant-design_rocket-outlined",
"ant-design_rollback-outlined",
"ant-design_rotate-left-outlined",
"ant-design_rotate-right-outlined",
"ant-design_ruby-outlined",
"ant-design_safety-certificate-outlined",
"ant-design_safety-outlined",
"ant-design_save-outlined",
"ant-design_scan-outlined",
"ant-design_schedule-outlined",
"ant-design_scissor-outlined",
"ant-design_search-outlined",
"ant-design_security-scan-outlined",
"ant-design_select-outlined",
"ant-design_send-outlined",
"ant-design_setting-outlined",
"ant-design_shake-outlined",
"ant-design_share-alt-outlined",
"ant-design_shop-outlined",
"ant-design_shopping-cart-outlined",
"ant-design_shopping-outlined",
"ant-design_shrink-outlined",
"ant-design_signature-outlined",
"ant-design_sisternode-outlined",
"ant-design_sketch-outlined",
"ant-design_skin-outlined",
"ant-design_skype-outlined",
"ant-design_slack-outlined",
"ant-design_slack-square-outlined",
"ant-design_sliders-outlined",
"ant-design_small-dash-outlined",
"ant-design_smile-outlined",
"ant-design_snippets-outlined",
"ant-design_solution-outlined",
"ant-design_sort-ascending-outlined",
"ant-design_sort-descending-outlined",
"ant-design_sound-outlined",
"ant-design_split-cells-outlined",
"ant-design_spotify-outlined",
"ant-design_star-outlined",
"ant-design_step-backward-outlined",
"ant-design_step-forward-outlined",
"ant-design_stock-outlined",
"ant-design_stop-outlined",
"ant-design_strikethrough-outlined",
"ant-design_subnode-outlined",
"ant-design_sun-outlined",
"ant-design_swap-left-outlined",
"ant-design_swap-outlined",
"ant-design_swap-right-outlined",
"ant-design_switcher-outlined",
"ant-design_sync-outlined",
"ant-design_table-outlined",
"ant-design_tablet-outlined",
"ant-design_tag-outlined",
"ant-design_tags-outlined",
"ant-design_taobao-circle-outlined",
"ant-design_taobao-outlined",
"ant-design_team-outlined",
"ant-design_thunderbolt-outlined",
"ant-design_tik-tok-outlined",
"ant-design_to-top-outlined",
"ant-design_tool-outlined",
"ant-design_trademark-circle-outlined",
"ant-design_trademark-outlined",
"ant-design_transaction-outlined",
"ant-design_translation-outlined",
"ant-design_trophy-outlined",
"ant-design_truck-outlined",
"ant-design_twitch-outlined",
"ant-design_twitter-outlined",
"ant-design_underline-outlined",
"ant-design_undo-outlined",
"ant-design_ungroup-outlined",
"ant-design_unlock-outlined",
"ant-design_unordered-list-outlined",
"ant-design_up-circle-outlined",
"ant-design_up-outlined",
"ant-design_up-square-outlined",
"ant-design_upload-outlined",
"ant-design_usb-outlined",
"ant-design_user-add-outlined",
"ant-design_user-delete-outlined",
"ant-design_user-outlined",
"ant-design_user-switch-outlined",
"ant-design_usergroup-add-outlined",
"ant-design_usergroup-delete-outlined",
"ant-design_verified-outlined",
"ant-design_vertical-align-bottom-outlined",
"ant-design_vertical-align-middle-outlined",
"ant-design_vertical-align-top-outlined",
"ant-design_vertical-left-outlined",
"ant-design_vertical-right-outlined",
"ant-design_video-camera-add-outlined",
"ant-design_video-camera-outlined",
"ant-design_wallet-outlined",
"ant-design_warning-outlined",
"ant-design_wechat-outlined",
"ant-design_wechat-work-outlined",
"ant-design_weibo-circle-outlined",
"ant-design_weibo-outlined",
"ant-design_weibo-square-outlined",
"ant-design_whats-app-outlined",
"ant-design_wifi-outlined",
"ant-design_windows-outlined",
"ant-design_woman-outlined",
"ant-design_x-outlined",
"ant-design_yahoo-outlined",
"ant-design_youtube-outlined",
"ant-design_yuque-outlined",
"ant-design_zhihu-outlined",
"ant-design_zoom-in-outlined",
"ant-design_zoom-out-outlined"
]
},
{
name: "实底风格",
key: "filled",
item: [
"ant-design_account-book-filled",
"ant-design_alert-filled",
"ant-design_alipay-circle-filled",
"ant-design_alipay-square-filled",
"ant-design_aliwangwang-filled",
"ant-design_amazon-circle-filled",
"ant-design_amazon-square-filled",
"ant-design_android-filled",
"ant-design_api-filled",
"ant-design_apple-filled",
"ant-design_appstore-filled",
"ant-design_audio-filled",
"ant-design_backward-filled",
"ant-design_bank-filled",
"ant-design_behance-circle-filled",
"ant-design_behance-square-filled",
"ant-design_bell-filled",
"ant-design_bilibili-filled",
"ant-design_book-filled",
"ant-design_box-plot-filled",
"ant-design_bug-filled",
"ant-design_build-filled",
"ant-design_bulb-filled",
"ant-design_calculator-filled",
"ant-design_calendar-filled",
"ant-design_camera-filled",
"ant-design_car-filled",
"ant-design_caret-down-filled",
"ant-design_caret-left-filled",
"ant-design_caret-right-filled",
"ant-design_caret-up-filled",
"ant-design_carry-out-filled",
"ant-design_check-circle-filled",
"ant-design_check-square-filled",
"ant-design_chrome-filled",
"ant-design_ci-circle-filled",
"ant-design_clock-circle-filled",
"ant-design_close-circle-filled",
"ant-design_close-square-filled",
"ant-design_cloud-filled",
"ant-design_code-filled",
"ant-design_code-sandbox-circle-filled",
"ant-design_code-sandbox-square-filled",
"ant-design_codepen-circle-filled",
"ant-design_codepen-square-filled",
"ant-design_compass-filled",
"ant-design_contacts-filled",
"ant-design_container-filled",
"ant-design_control-filled",
"ant-design_copy-filled",
"ant-design_copyright-circle-filled",
"ant-design_credit-card-filled",
"ant-design_crown-filled",
"ant-design_customer-service-filled",
"ant-design_dashboard-filled",
"ant-design_database-filled",
"ant-design_delete-filled",
"ant-design_diff-filled",
"ant-design_dingtalk-circle-filled",
"ant-design_dingtalk-square-filled",
"ant-design_discord-filled",
"ant-design_dislike-filled",
"ant-design_dollar-circle-filled",
"ant-design_down-circle-filled",
"ant-design_down-square-filled",
"ant-design_dribbble-circle-filled",
"ant-design_dribbble-square-filled",
"ant-design_dropbox-circle-filled",
"ant-design_dropbox-square-filled",
"ant-design_edit-filled",
"ant-design_environment-filled",
"ant-design_euro-circle-filled",
"ant-design_exclamation-circle-filled",
"ant-design_experiment-filled",
"ant-design_eye-filled",
"ant-design_eye-invisible-filled",
"ant-design_facebook-filled",
"ant-design_fast-backward-filled",
"ant-design_fast-forward-filled",
"ant-design_file-add-filled",
"ant-design_file-excel-filled",
"ant-design_file-exclamation-filled",
"ant-design_file-filled",
"ant-design_file-image-filled",
"ant-design_file-markdown-filled",
"ant-design_file-pdf-filled",
"ant-design_file-ppt-filled",
"ant-design_file-text-filled",
"ant-design_file-unknown-filled",
"ant-design_file-word-filled",
"ant-design_file-zip-filled",
"ant-design_filter-filled",
"ant-design_fire-filled",
"ant-design_flag-filled",
"ant-design_folder-add-filled",
"ant-design_folder-filled",
"ant-design_folder-open-filled",
"ant-design_format-painter-filled",
"ant-design_forward-filled",
"ant-design_frown-filled",
"ant-design_fund-filled",
"ant-design_funnel-plot-filled",
"ant-design_gift-filled",
"ant-design_github-filled",
"ant-design_gitlab-filled",
"ant-design_gold-filled",
"ant-design_golden-filled",
"ant-design_google-circle-filled",
"ant-design_google-plus-circle-filled",
"ant-design_google-plus-square-filled",
"ant-design_google-square-filled",
"ant-design_hdd-filled",
"ant-design_heart-filled",
"ant-design_highlight-filled",
"ant-design_home-filled",
"ant-design_hourglass-filled",
"ant-design_html5-filled",
"ant-design_idcard-filled",
"ant-design_ie-circle-filled",
"ant-design_ie-square-filled",
"ant-design_info-circle-filled",
"ant-design_instagram-filled",
"ant-design_insurance-filled",
"ant-design_interaction-filled",
"ant-design_layout-filled",
"ant-design_left-circle-filled",
"ant-design_left-square-filled",
"ant-design_like-filled",
"ant-design_linkedin-filled",
"ant-design_lock-filled",
"ant-design_mac-command-filled",
"ant-design_mail-filled",
"ant-design_medicine-box-filled",
"ant-design_medium-circle-filled",
"ant-design_medium-square-filled",
"ant-design_meh-filled",
"ant-design_merge-filled",
"ant-design_message-filled",
"ant-design_minus-circle-filled",
"ant-design_minus-square-filled",
"ant-design_mobile-filled",
"ant-design_money-collect-filled",
"ant-design_moon-filled",
"ant-design_muted-filled",
"ant-design_notification-filled",
"ant-design_open-a-i-filled",
"ant-design_pause-circle-filled",
"ant-design_pay-circle-filled",
"ant-design_phone-filled",
"ant-design_picture-filled",
"ant-design_pie-chart-filled",
"ant-design_pinterest-filled",
"ant-design_play-circle-filled",
"ant-design_play-square-filled",
"ant-design_plus-circle-filled",
"ant-design_plus-square-filled",
"ant-design_pound-circle-filled",
"ant-design_printer-filled",
"ant-design_product-filled",
"ant-design_profile-filled",
"ant-design_project-filled",
"ant-design_property-safety-filled",
"ant-design_pushpin-filled",
"ant-design_qq-circle-filled",
"ant-design_qq-square-filled",
"ant-design_question-circle-filled",
"ant-design_read-filled",
"ant-design_reconciliation-filled",
"ant-design_red-envelope-filled",
"ant-design_reddit-circle-filled",
"ant-design_reddit-square-filled",
"ant-design_rest-filled",
"ant-design_right-circle-filled",
"ant-design_right-square-filled",
"ant-design_robot-filled",
"ant-design_rocket-filled",
"ant-design_safety-certificate-filled",
"ant-design_save-filled",
"ant-design_schedule-filled",
"ant-design_security-scan-filled",
"ant-design_setting-filled",
"ant-design_shop-filled",
"ant-design_shopping-filled",
"ant-design_signal-filled",
"ant-design_signature-filled",
"ant-design_sketch-circle-filled",
"ant-design_sketch-square-filled",
"ant-design_skin-filled",
"ant-design_skype-filled",
"ant-design_slack-circle-filled",
"ant-design_slack-square-filled",
"ant-design_sliders-filled",
"ant-design_smile-filled",
"ant-design_snippets-filled",
"ant-design_sound-filled",
"ant-design_spotify-filled",
"ant-design_star-filled",
"ant-design_step-backward-filled",
"ant-design_step-forward-filled",
"ant-design_stop-filled",
"ant-design_sun-filled",
"ant-design_switcher-filled",
"ant-design_tablet-filled",
"ant-design_tag-filled",
"ant-design_tags-filled",
"ant-design_taobao-circle-filled",
"ant-design_taobao-square-filled",
"ant-design_thunderbolt-filled",
"ant-design_tik-tok-filled",
"ant-design_tool-filled",
"ant-design_trademark-circle-filled",
"ant-design_trophy-filled",
"ant-design_truck-filled",
"ant-design_twitter-circle-filled",
"ant-design_twitter-square-filled",
"ant-design_unlock-filled",
"ant-design_up-circle-filled",
"ant-design_up-square-filled",
"ant-design_usb-filled",
"ant-design_video-camera-filled",
"ant-design_wallet-filled",
"ant-design_warning-filled",
"ant-design_wechat-filled",
"ant-design_wechat-work-filled",
"ant-design_weibo-circle-filled",
"ant-design_weibo-square-filled",
"ant-design_windows-filled",
"ant-design_x-filled",
"ant-design_yahoo-filled",
"ant-design_youtube-filled",
"ant-design_yuque-filled",
"ant-design_zhihu-circle-filled",
"ant-design_zhihu-square-filled"
]
},
{
name: "双色风格",
key: "twotone",
item: [
"ant-design_account-book-twotone",
"ant-design_alert-twotone",
"ant-design_api-twotone",
"ant-design_appstore-twotone",
"ant-design_audio-twotone",
"ant-design_bank-twotone",
"ant-design_bell-twotone",
"ant-design_book-twotone",
"ant-design_box-plot-twotone",
"ant-design_bug-twotone",
"ant-design_build-twotone",
"ant-design_bulb-twotone",
"ant-design_calculator-twotone",
"ant-design_calendar-twotone",
"ant-design_camera-twotone",
"ant-design_car-twotone",
"ant-design_carry-out-twotone",
"ant-design_check-circle-twotone",
"ant-design_check-square-twotone",
"ant-design_ci-circle-twotone",
"ant-design_ci-twotone",
"ant-design_clock-circle-twotone",
"ant-design_close-circle-twotone",
"ant-design_close-square-twotone",
"ant-design_cloud-twotone",
"ant-design_code-twotone",
"ant-design_compass-twotone",
"ant-design_contacts-twotone",
"ant-design_container-twotone",
"ant-design_control-twotone",
"ant-design_copy-twotone",
"ant-design_copyright-circle-twotone",
"ant-design_copyright-twotone",
"ant-design_credit-card-twotone",
"ant-design_crown-twotone",
"ant-design_customer-service-twotone",
"ant-design_dashboard-twotone",
"ant-design_database-twotone",
"ant-design_delete-twotone",
"ant-design_diff-twotone",
"ant-design_dislike-twotone",
"ant-design_dollar-circle-twotone",
"ant-design_dollar-twotone",
"ant-design_down-circle-twotone",
"ant-design_down-square-twotone",
"ant-design_edit-twotone",
"ant-design_environment-twotone",
"ant-design_euro-circle-twotone",
"ant-design_euro-twotone",
"ant-design_exclamation-circle-twotone",
"ant-design_experiment-twotone",
"ant-design_eye-invisible-twotone",
"ant-design_eye-twotone",
"ant-design_file-add-twotone",
"ant-design_file-excel-twotone",
"ant-design_file-exclamation-twotone",
"ant-design_file-image-twotone",
"ant-design_file-markdown-twotone",
"ant-design_file-pdf-twotone",
"ant-design_file-ppt-twotone",
"ant-design_file-text-twotone",
"ant-design_file-twotone",
"ant-design_file-unknown-twotone",
"ant-design_file-word-twotone",
"ant-design_file-zip-twotone",
"ant-design_filter-twotone",
"ant-design_fire-twotone",
"ant-design_flag-twotone",
"ant-design_folder-add-twotone",
"ant-design_folder-open-twotone",
"ant-design_folder-twotone",
"ant-design_frown-twotone",
"ant-design_fund-twotone",
"ant-design_funnel-plot-twotone",
"ant-design_gift-twotone",
"ant-design_gold-twotone",
"ant-design_hdd-twotone",
"ant-design_heart-twotone",
"ant-design_highlight-twotone",
"ant-design_home-twotone",
"ant-design_hourglass-twotone",
"ant-design_html5-twotone",
"ant-design_idcard-twotone",
"ant-design_info-circle-twotone",
"ant-design_insurance-twotone",
"ant-design_interaction-twotone",
"ant-design_layout-twotone",
"ant-design_left-circle-twotone",
"ant-design_left-square-twotone",
"ant-design_like-twotone",
"ant-design_lock-twotone",
"ant-design_mail-twotone",
"ant-design_medicine-box-twotone",
"ant-design_meh-twotone",
"ant-design_message-twotone",
"ant-design_minus-circle-twotone",
"ant-design_minus-square-twotone",
"ant-design_mobile-twotone",
"ant-design_money-collect-twotone",
"ant-design_notification-twotone",
"ant-design_pause-circle-twotone",
"ant-design_phone-twotone",
"ant-design_picture-twotone",
"ant-design_pie-chart-twotone",
"ant-design_play-circle-twotone",
"ant-design_play-square-twotone",
"ant-design_plus-circle-twotone",
"ant-design_plus-square-twotone",
"ant-design_pound-circle-twotone",
"ant-design_printer-twotone",
"ant-design_profile-twotone",
"ant-design_project-twotone",
"ant-design_property-safety-twotone",
"ant-design_pushpin-twotone",
"ant-design_question-circle-twotone",
"ant-design_reconciliation-twotone",
"ant-design_red-envelope-twotone",
"ant-design_rest-twotone",
"ant-design_right-circle-twotone",
"ant-design_right-square-twotone",
"ant-design_rocket-twotone",
"ant-design_safety-certificate-twotone",
"ant-design_save-twotone",
"ant-design_schedule-twotone",
"ant-design_security-scan-twotone",
"ant-design_setting-twotone",
"ant-design_shop-twotone",
"ant-design_shopping-twotone",
"ant-design_skin-twotone",
"ant-design_sliders-twotone",
"ant-design_smile-twotone",
"ant-design_snippets-twotone",
"ant-design_sound-twotone",
"ant-design_star-twotone",
"ant-design_stop-twotone",
"ant-design_switcher-twotone",
"ant-design_tablet-twotone",
"ant-design_tag-twotone",
"ant-design_tags-twotone",
"ant-design_thunderbolt-twotone",
"ant-design_tool-twotone",
"ant-design_trademark-circle-twotone",
"ant-design_trophy-twotone",
"ant-design_unlock-twotone",
"ant-design_up-circle-twotone",
"ant-design_up-square-twotone",
"ant-design_usb-twotone",
"ant-design_video-camera-twotone",
"ant-design_wallet-twotone",
"ant-design_warning-twotone"
]
}
]
},
{
name: "扩展",
key: "extend",
iconItem: [
{
name: "常用",
key: "default",
item: []
},
{
name: "其他",
key: "other",
item: ["alipay"]
}
]
}
]
}
@@ -0,0 +1,133 @@
<template>
<el-dialog title="图标选择" :visible.sync="visible" width="800px" :close-on-click-modal="false" :modal="false">
<el-tabs v-model="activeKey" tab-position="left" size="small" @tab-click="activeChange">
<el-tab-pane v-for="item in iconData" :key="item.key" :label="item.name" :name="item.key" class="pl25">
<div v-if="item.iconItem.length > 1" class="xn-icon-select-radio">
<el-radio-group v-model="iconItemDefault" @change="radioGroupChange" size="small">
<el-radio v-for="iconItem in item.iconItem" :key="iconItem.key" :label="iconItem.key" border>
{{ iconItem.name }}
</el-radio>
</el-radio-group>
</div>
<div :key="iconItemIns.key" v-for="iconItemIns in item.iconItem">
<div v-show="iconItemIns.key === iconItemDefault" class="xn-icon-select-list">
<ul style="white-space: normal">
<li v-for="icon in iconItemIns.item" :key="icon" :class="icon === value ? 'active' : ''" @click="selectIcon(icon)">
<div :id="icon" @mouseover="iconInfo = icon">
<el-image :src="'/assets/platform/img/svg/' + icon + '.svg'" alt="" class="xn-icons">
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</div>
</li>
</ul>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<script>
module.exports = {
name: "index",
props: {
value: {
type: String,
default: ""
}
},
data() {
return {
visible: false,
iconData: [],
activeKey: "default",
iconItemDefault: "default",
iconInfo: null
}
},
methods: {
activeChange(val) {},
radioGroupChange(e) {
console.log(e)
},
//打开
showIconModal() {
this.visible = true
},
defaultSetting(value) {
if (value) {
this.value = value
// 判断展开哪个
if (value.indexOf("-outlined") > -1 || value.indexOf("-filled") > -1 || value.indexOf("-two-tone") > -1) {
this.activeKey = "default"
if (value.indexOf("-two-tone") > -1) {
this.iconItemDefault = "twotone"
} else if (value.indexOf("-filled") > -1) {
this.iconItemDefault = "filled"
}
} else if (value.indexOf("-extend") > -1) {
// 扩展列表
this.activeKey = "extend"
// 如扩展其他顶部单选的情况,默认选中在这里配置,同时这里需要做判断
// this.iconItemDefault = '您的json中配置的'
}
}
},
// 选择图标后关闭并返回
selectIcon(icon) {
this.visible = false
this.$emit("input", icon)
}
},
mounted() {
this.iconData.push(...window.iconSelectConfig.icons)
},
created() {}
}
</script>
<style scoped>
.xn-icon-select-radio {
padding-left: 5px;
padding-bottom: 10px;
}
.xn-icons {
/*font-size: 26px;*/
/*width: 100%;*/
/*height: 100%;*/
height: 26px;
width: 26px;
display: flex;
justify-content: center;
align-items: center;
}
.xn-icon-select-list {
height: 360px;
overflow: auto;
}
.xn-icon-select-list ul li {
display: inline-block;
width: 60px;
height: 60px;
padding: 18px;
margin: 5px;
border-radius: 2px;
vertical-align: top;
box-shadow: 0 0 0 1px rgba(5, 5, 5, 0.06);
transition: all 0.1s;
position: relative;
box-sizing: border-box;
}
.xn-icon-select-list ul li:hover,
.xn-icon-select-list ul li.active {
cursor: pointer;
color: #ffffff;
background-color: var(--color-primary);
}
</style>
@@ -0,0 +1,994 @@
<script>
module.exports = {
name: "excelImport",
props: {
url: {
type: String,
required: true,
description: "后端接收文件的URL"
},
extra_params: {
type: Object,
default: () => ({}),
description: "上传时附加的参数"
},
template_url: {
type: String,
default: "",
description: "下载导入模板的URL地址"
},
mode: {
type: String,
default: "dialog",
validator: (value) => ["dialog", "direct"].includes(value),
description: "显示模式:dialog-弹窗模式(默认)direct-直接显示"
},
title: {
type: String,
default: "Excel 数据导入",
description: "弹窗标题(dialog模式下有效)"
},
width: {
type: String,
default: "800px",
description: "弹窗宽度(dialog模式下有效)"
},
visible: {
type: Boolean,
default: false,
description: "是否显示弹窗(dialog模式下有效)"
}
},
data() {
return {
selectedFile: null,
importing: false,
importComplete: false,
progress: 0,
progressText: "",
stats: {
totalRecords: 0,
successCount: 0,
failedCount: 0
},
errorDetails: [],
excelData: [],
dialogVisible: this.visible,
downloading: false
}
},
computed: {
progressColor() {
if (this.progress < 30) return "#F56C6C"
if (this.progress < 70) return "#E6A23C"
return "#67C23A"
},
successRate() {
if (this.stats.totalRecords === 0) return 0
return Math.round((this.stats.successCount / this.stats.totalRecords) * 100)
},
showTemplateBtn() {
return !!this.template_url
}
},
watch: {
visible(val) {
this.dialogVisible = val
},
dialogVisible(val) {
if (val !== this.visible) {
this.$emit("update:visible", val)
}
if (!val) {
this.resetUpload()
}
}
},
methods: {
handleFileChange(file) {
this.selectedFile = file.raw
this.resetStats()
},
beforeUpload(file) {
const isExcel =
file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
file.type === "application/vnd.ms-excel" ||
file.name.toLowerCase().endsWith(".xlsx") ||
file.name.toLowerCase().endsWith(".xls")
if (!isExcel) {
this.$message.error("只能上传 Excel 文件!")
return false
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
this.$message.error("文件大小不能超过 10MB")
return false
}
return false // 阻止自动上传
},
async startImport() {
if (!this.selectedFile) {
this.$message.warning("请先选择文件!")
return
}
if (!this.url) {
this.$message.error("未配置上传URL")
return
}
this.importing = true
this.progress = 0
this.progressText = "准备上传文件..."
try {
// 创建FormData对象
const formData = new FormData()
formData.append("file", this.selectedFile)
// 添加额外参数
if (this.extra_params) {
Object.keys(this.extra_params).forEach((key) => {
formData.append(key, this.extra_params[key])
})
}
// 设置上传进度
await this.updateProgress(20, "正在上传文件...")
// 发送请求到后端
const { code, data } = await this.$axios.post(this.url, formData, {
headers: {
"Content-Type": "multipart/form-data"
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 40) / progressEvent.total)
this.updateProgress(20 + percentCompleted, "上传中...")
}
})
// 处理后端返回的结果
await this.updateProgress(70, "处理后端导入结果...")
if (code === 0 && data) {
// 处理成功
const result = data || {}
this.stats.totalRecords = result.totalRecords || 0
this.stats.successCount = result.successCount || 0
this.stats.failedCount = result.failedCount || 0
this.errorDetails = result.errorDetails || []
await this.updateProgress(100, "导入完成!")
setTimeout(() => {
this.importing = false
this.importComplete = true
this.$message.success("Excel 导入完成!")
this.$emit("import-success", result)
}, 500)
} else {
// 处理失败
throw new Error(msg || "导入失败")
}
} catch (error) {
this.importing = false
this.$message.error("导入失败:" + (error.message || "未知错误"))
this.$emit("import-error", error)
}
},
async downloadTemplate() {
if (!this.template_url) {
this.$message.warning("未配置模板下载地址!")
return
}
this.downloading = true
try {
// 下载模板文件
const response = await this.$axios({
url: this.template_url,
method: "GET",
responseType: "blob"
})
// 创建下载链接
const blob = new Blob([response.data])
const url = window.URL.createObjectURL(blob)
// 获取文件名,从Content-Disposition中提取或使用默认名称
let filename = "导入模板.xlsx"
const disposition = response.headers["content-disposition"]
if (disposition && disposition.indexOf("attachment") !== -1) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
const matches = filenameRegex.exec(disposition)
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, "")
// 解决中文乱码问题
try {
filename = decodeURIComponent(filename)
} catch (e) {
// 解码失败时使用原始文件名
}
}
}
// 触发下载
const link = document.createElement("a")
link.href = url
link.download = filename
link.style.display = "none"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
this.$message.success("模板下载成功!")
} catch (error) {
this.$message.error("模板下载失败:" + (error.message || "未知错误"))
this.$emit("template-error", error)
} finally {
this.downloading = false
}
},
readExcelFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: "array" })
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 })
resolve(jsonData)
} catch (error) {
reject(new Error("文件解析失败"))
}
}
reader.onerror = () => reject(new Error("文件读取失败"))
reader.readAsArrayBuffer(file)
})
},
async processData(data) {
this.excelData = data
this.stats.totalRecords = data.length - 1 // 减去表头
this.errorDetails = []
let successCount = 0
let failedCount = 0
// 模拟数据处理过程
for (let i = 1; i < data.length; i++) {
await this.updateProgress(40 + Math.round((i / (data.length - 1)) * 50), `处理第 ${i} 条记录...`)
// 模拟数据验证和处理
const row = data[i]
const isValid = this.validateRow(row, i + 1)
if (isValid) {
successCount++
} else {
failedCount++
}
// 添加延迟以显示进度效果
if (i % 10 === 0) {
await new Promise((resolve) => setTimeout(resolve, 50))
}
}
this.stats.successCount = successCount
this.stats.failedCount = failedCount
},
validateRow(row, rowIndex) {
// 模拟数据验证逻辑
if (!row || row.length === 0) {
this.errorDetails.push({
row: rowIndex,
message: "空行数据"
})
return false
}
// 随机生成一些错误(用于演示)
if (Math.random() < 0.1) {
// 10% 的错误率
const errors = ["必填字段为空", "数据格式不正确", "数据长度超出限制", "重复数据", "外键约束违反"]
this.errorDetails.push({
row: rowIndex,
message: errors[Math.floor(Math.random() * errors.length)]
})
return false
}
return true
},
updateProgress(percentage, text) {
return new Promise((resolve) => {
setTimeout(() => {
this.progress = percentage
this.progressText = text
resolve()
}, 100)
})
},
getSuccessRateColor(rate) {
if (rate >= 90) return "#67C23A"
if (rate >= 70) return "#E6A23C"
return "#F56C6C"
},
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes"
const k = 1024
const sizes = ["Bytes", "KB", "MB", "GB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
},
downloadReport() {
if (this.errorDetails.length === 0) return
let content = "错误报告\n\n"
content += `总记录数: ${this.stats.totalRecords}\n`
content += `成功数: ${this.stats.successCount}\n`
content += `失败数: ${this.stats.failedCount}\n`
content += `成功率: ${this.successRate}%\n\n`
content += "错误详情:\n"
this.errorDetails.forEach((error) => {
content += `${error.row}行: ${error.message}\n`
})
const blob = new Blob([content], { type: "text/plain;charset=utf-8" })
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = "导入错误报告.txt"
a.click()
window.URL.revokeObjectURL(url)
this.$message.success("错误报告下载完成!")
},
resetUpload() {
this.selectedFile = null
this.importing = false
this.importComplete = false
this.progress = 0
this.progressText = ""
this.resetStats()
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
this.$emit("reset")
},
resetStats() {
this.stats = {
totalRecords: 0,
successCount: 0,
failedCount: 0
}
this.errorDetails = []
this.excelData = []
},
closeDialog() {
if (this.mode === "dialog") {
this.dialogVisible = false
}
},
showDialog() {
if (this.mode === "dialog") {
this.dialogVisible = true
}
}
}
}
</script>
<template>
<!-- Dialog模式 -->
<el-dialog
v-if="mode === 'dialog'"
:visible.sync="dialogVisible"
:title="title"
:width="width"
@close="resetUpload"
:append-to-body="true"
:destroy-on-close="true"
>
<div class="excel-import-container dialog-mode">
<div class="content">
<!-- 文件上传区域 -->
<div class="upload-area" v-if="!importing && !importComplete">
<el-upload
ref="upload"
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:before-upload="beforeUpload"
accept=".xlsx,.xls"
drag
:show-file-list="false"
>
<div class="upload-icon">📁</div>
<div class="upload-text"> Excel 文件拖拽到此处或点击选择文件</div>
<div class="upload-hint">支持 .xlsx.xls 格式文件大小不超过 10MB</div>
</el-upload>
</div>
<!-- 下载模板按钮 -->
<div v-if="showTemplateBtn && !importing && !importComplete" class="template-download">
<el-button type="text" @click="downloadTemplate" :loading="downloading" icon="el-icon-download">下载导入模板</el-button>
</div>
<!-- 已选择文件信息 -->
<div v-if="selectedFile && !importing && !importComplete" class="file-info">
<div class="file-info-item">
<span><strong>文件名:</strong></span>
<span>{{ selectedFile.name }}</span>
</div>
<div class="file-info-item">
<span><strong>文件大小:</strong></span>
<span>{{ formatFileSize(selectedFile.size) }}</span>
</div>
<div class="file-info-item">
<span><strong>文件类型:</strong></span>
<span>{{ selectedFile.type || "未知" }}</span>
</div>
</div>
<!-- 导入按钮 -->
<div v-if="selectedFile && !importing && !importComplete" class="actions">
<el-button type="primary" size="large" @click="startImport" :loading="importing">
<i class="el-icon-upload2"></i>
开始导入
</el-button>
<el-button size="large" @click="resetUpload">
<i class="el-icon-refresh"></i>
重新选择
</el-button>
</div>
<!-- 导入进度 -->
<div v-if="importing" class="progress-section">
<h3 style="text-align: center; color: #409eff; margin-bottom: 20px">
<i class="el-icon-loading"></i>
正在处理文件...
</h3>
<el-progress :percentage="progress" :color="progressColor" :stroke-width="8" text-inside></el-progress>
<p style="text-align: center; margin-top: 15px; color: #606266">
{{ progressText }}
</p>
</div>
<!-- 导入结果 -->
<div v-if="importComplete" class="result-section">
<div class="result-header">
<div class="result-icon">
<i class="el-icon-circle-check"></i>
</div>
<h3>导入完成</h3>
</div>
<div class="stats-container">
<div class="stat-card">
<div class="stat-icon total-icon">
<i class="el-icon-document"></i>
</div>
<div class="stat-content">
<div class="stat-number total">{{ stats.totalRecords }}</div>
<div class="stat-label">总记录数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon success-icon">
<i class="el-icon-check"></i>
</div>
<div class="stat-content">
<div class="stat-number success">{{ stats.successCount }}</div>
<div class="stat-label">导入成功</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon failed-icon">
<i class="el-icon-close"></i>
</div>
<div class="stat-content">
<div class="stat-number failed">{{ stats.failedCount }}</div>
<div class="stat-label">导入失败</div>
</div>
</div>
</div>
<!-- 成功率展示 -->
<div class="success-rate-section">
<div class="success-rate-header">
<span class="success-rate-label">导入成功率</span>
<span class="success-rate-value" :style="{ color: getSuccessRateColor(successRate) }">{{ successRate }}%</span>
</div>
<el-progress :percentage="successRate" :color="getSuccessRateColor(successRate)" :stroke-width="6"></el-progress>
</div>
<!-- 错误详情 -->
<div v-if="errorDetails.length > 0" class="error-details-section">
<el-collapse>
<el-collapse-item name="errors">
<template slot="title">
<div class="error-title">
<i class="el-icon-warning-outline"></i>
<span>查看错误详情 ({{ errorDetails.length }})</span>
</div>
</template>
<div class="error-list">
<div v-for="(error, index) in errorDetails" :key="index" class="error-item">
<div class="error-row"> {{ error.row }} </div>
<div class="error-message">{{ error.errMsg }}</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
<!-- 操作按钮 -->
<div class="actions">
<el-button type="primary" @click="downloadReport" v-if="stats.failedCount > 0">
<i class="el-icon-download"></i>
下载错误报告
</el-button>
<el-button @click="resetUpload">
<i class="el-icon-refresh"></i>
重新导入
</el-button>
<el-button @click="closeDialog">
<i class="el-icon-close"></i>
关闭
</el-button>
</div>
</div>
</div>
</div>
</el-dialog>
<!-- 直接显示模式 -->
<div v-else class="excel-import-container">
<div class="header">
<h1>📊 Excel 文件导入</h1>
<p>支持 .xlsx.xls 格式文件自动解析并统计导入结果</p>
</div>
<div class="content">
<!-- 文件上传区域 -->
<div class="upload-area" v-if="!importing && !importComplete">
<el-upload
ref="upload"
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:before-upload="beforeUpload"
accept=".xlsx,.xls"
drag
:show-file-list="false"
>
<div class="upload-icon">📁</div>
<div class="upload-text"> Excel 文件拖拽到此处或点击选择文件</div>
<div class="upload-hint">支持 .xlsx.xls 格式文件大小不超过 10MB</div>
</el-upload>
</div>
<!-- 下载模板按钮 -->
<div v-if="showTemplateBtn && !importing && !importComplete" class="template-download">
<el-button type="text" @click="downloadTemplate" :loading="downloading" icon="el-icon-download">下载导入模板</el-button>
</div>
<!-- 已选择文件信息 -->
<div v-if="selectedFile && !importing && !importComplete" class="file-info">
<div class="file-info-item">
<span><strong>文件名:</strong></span>
<span>{{ selectedFile.name }}</span>
</div>
<div class="file-info-item">
<span><strong>文件大小:</strong></span>
<span>{{ formatFileSize(selectedFile.size) }}</span>
</div>
<div class="file-info-item">
<span><strong>文件类型:</strong></span>
<span>{{ selectedFile.type || "未知" }}</span>
</div>
</div>
<!-- 导入按钮 -->
<div v-if="selectedFile && !importing && !importComplete" class="actions">
<el-button type="primary" size="large" @click="startImport" :loading="importing">
<i class="el-icon-upload2"></i>
开始导入
</el-button>
<el-button size="large" @click="resetUpload">
<i class="el-icon-refresh"></i>
重新选择
</el-button>
</div>
<!-- 导入进度 -->
<div v-if="importing" class="progress-section">
<h3 style="text-align: center; color: #409eff; margin-bottom: 20px">
<i class="el-icon-loading"></i>
正在处理文件...
</h3>
<el-progress :percentage="progress" :color="progressColor" :stroke-width="8" text-inside></el-progress>
<p style="text-align: center; margin-top: 15px; color: #606266">
{{ progressText }}
</p>
</div>
<!-- 导入结果 -->
<div v-if="importComplete" class="result-section">
<div class="result-header">
<div class="result-icon">
<i class="el-icon-circle-check"></i>
</div>
<h3>导入完成</h3>
</div>
<div class="stats-container">
<div class="stat-card">
<div class="stat-icon total-icon">
<i class="el-icon-document"></i>
</div>
<div class="stat-content">
<div class="stat-number total">{{ stats.totalRecords }}</div>
<div class="stat-label">总记录数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon success-icon">
<i class="el-icon-check"></i>
</div>
<div class="stat-content">
<div class="stat-number success">{{ stats.successCount }}</div>
<div class="stat-label">导入成功</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon failed-icon">
<i class="el-icon-close"></i>
</div>
<div class="stat-content">
<div class="stat-number failed">{{ stats.failedCount }}</div>
<div class="stat-label">导入失败</div>
</div>
</div>
</div>
<!-- 错误详情 -->
<div v-if="errorDetails.length > 0" class="error-details-section">
<el-collapse>
<el-collapse-item name="errors">
<template slot="title">
<div class="error-title">
<i class="el-icon-warning-outline"></i>
<span>查看错误详情 ({{ errorDetails.length }})</span>
</div>
</template>
<div class="error-list">
<div v-for="(error, index) in errorDetails" :key="index" class="error-item">
<div class="error-row"> {{ error.row }} </div>
<div class="error-message">{{ error.message }}</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
<!-- 操作按钮 -->
<div class="actions">
<el-button type="primary" @click="downloadReport" v-if="stats.failedCount > 0">
<i class="el-icon-download"></i>
下载错误报告
</el-button>
<el-button @click="resetUpload">
<i class="el-icon-refresh"></i>
重新导入
</el-button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.excel-import-container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.excel-import-container.dialog-mode {
max-width: 100%;
margin: 0;
box-shadow: none;
border-radius: 0;
}
.header {
background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 28px;
font-weight: 300;
}
.header p {
margin: 10px 0 0 0;
opacity: 0.9;
font-size: 14px;
}
.content {
padding: 40px;
}
.template-download {
text-align: center;
margin-bottom: 20px;
}
.upload-area {
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 40px;
text-align: center;
background: #fafafa;
transition: all 0.3s ease;
margin-bottom: 15px;
}
.upload-area:hover {
border-color: #409eff;
background: #f0f9ff;
}
.upload-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
}
.upload-text {
color: #606266;
font-size: 16px;
margin-bottom: 8px;
}
.upload-hint {
color: #909399;
font-size: 14px;
}
.progress-section {
margin: 30px 0;
}
.stats-container {
display: flex;
justify-content: space-between;
gap: 15px;
margin-top: 20px;
}
.stat-card {
flex: 1;
background: linear-gradient(135deg, #f6f8fa 0%, #ffffff 100%);
border: 1px solid #e1e8ed;
border-radius: 8px;
padding: 15px;
transition: transform 0.2s ease;
display: flex;
align-items: center;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
}
.total-icon {
background-color: rgba(64, 158, 255, 0.1);
color: #409eff;
}
.success-icon {
background-color: rgba(103, 194, 58, 0.1);
color: #67c23a;
}
.failed-icon {
background-color: rgba(245, 108, 108, 0.1);
color: #f56c6c;
}
.stat-content {
text-align: left;
}
.stat-number {
font-size: 24px;
font-weight: bold;
line-height: 1;
margin-bottom: 4px;
}
.total {
color: #409eff;
}
.success {
color: #67c23a;
}
.failed {
color: #f56c6c;
}
.stat-label {
color: #606266;
font-size: 12px;
}
.result-section {
margin-top: 30px;
padding: 30px;
background: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
}
.result-header {
text-align: center;
margin-bottom: 30px;
}
.result-icon {
font-size: 48px;
color: #67c23a;
margin-bottom: 10px;
}
.result-header h3 {
font-size: 20px;
font-weight: 500;
margin: 0;
color: #67c23a;
}
.success-rate-section {
margin-top: 30px;
background: #f9f9f9;
border-radius: 8px;
padding: 20px;
}
.success-rate-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.success-rate-label {
font-weight: 500;
font-size: 14px;
color: #606266;
}
.success-rate-value {
font-weight: bold;
font-size: 18px;
}
.error-details-section {
margin-top: 20px;
}
.error-title {
display: flex;
align-items: center;
color: #f56c6c;
font-weight: 500;
}
.error-title i {
margin-right: 8px;
font-size: 16px;
}
.error-list {
max-height: 250px;
overflow-y: auto;
background: #fff5f5;
border-radius: 4px;
}
.error-item {
padding: 12px;
border-bottom: 1px solid #fde2e2;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
}
.error-item:last-child {
border-bottom: none;
}
.error-row {
color: #f56c6c;
font-weight: bold;
background: rgba(245, 108, 108, 0.1);
padding: 2px 8px;
border-radius: 4px;
margin-right: 8px;
}
.error-message {
color: #606266;
flex: 1;
}
.file-info {
background: #e8f4fd;
border: 1px solid #b3d8ff;
border-radius: 6px;
padding: 16px;
margin-bottom: 20px;
}
.file-info-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.file-info-item:last-child {
margin-bottom: 0;
}
.actions {
text-align: center;
margin-top: 30px;
}
</style>
@@ -0,0 +1,213 @@
<template>
<div style="padding: 20px 50px">
<el-timeline>
<el-timeline-item timestamp="下载模板" placement="top">
<el-card>
<el-button size="medium" style="width: 200px" @click="$downLoad(temp_url)" icon="el-icon-download">下载模板</el-button>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="上传文件" placement="top">
<el-card>
<el-form>
<el-form-item label="请选择更新模式" v-if="is_show_radio">
<el-radio-group v-model="importData.isFlag">
<el-radio-button label="true">清空更新</el-radio-button>
<el-radio-button label="false">追加</el-radio-button>
</el-radio-group>
</el-form-item>
<el-upload
name="file"
ref="upload"
:on-remove="
(file, fileList) => {
importData.fileList = fileHandleRemove(file, fileList)
importResult = {
errorCount: 0,
successCount: 0,
totalCount: 0,
errorList: []
}
}
"
:on-change="
(file, fileList) => {
importData.fileList = fileHandleChange(file, fileList, { type: ['xls', 'xlsx'] })
}
"
:auto-upload="false"
:limit="1"
:file-list="importData.fileList"
>
<el-button size="medium" type="" icon="el-icon-upload" style="width: 200px">选择文件</el-button>
<div class="el-upload__tip" slot="tip" style="color: #f56c6c">只能上传 xls/xlsx 文件</div>
</el-upload>
</el-form>
</el-card>
</el-timeline-item>
<el-timeline-item placement="top" timestamp="导入结果">
<el-card shadow="never">
<p>总记录数:{{ errorInfoData.totalCount }}</p>
<p>
成功数:
<span class="text-success">{{ errorInfoData.successCount }}</span>
</p>
<p>
错误数:
<span class="text-danger">{{ errorInfoData.errorCount }}</span>
</p>
<el-link @click="exportErrors" type="primary" v-if="errorInfoData.errorCount > 0">下载错误记录</el-link>
</el-card>
</el-timeline-item>
</el-timeline>
<div style="text-align: right">
<span slot="footer" class="dialog-footer">
<!-- <el-button @click="importVisible = false" :disabled="importLoading">取 消</el-button>-->
<el-button type="primary" @click="doImport">确 定</el-button>
</span>
</div>
</div>
</template>
<script>
module.exports = {
props: {
temp_url: { type: String },
post_url: { type: String },
business_id: { type: String },
is_show_radio: { type: Boolean, default: false }
},
mounted() {
const s = document.createElement("script")
s.type = "text/javascript"
s.src = "/assets/platform/plugins/xlsx/xlsx.full.min.js"
document.body.appendChild(s)
},
data() {
return {
importData: {
fileList: [],
isFlag: false
},
errorInfoData: {
totalCount: 0,
successCount: 0,
errorCount: 0,
errorList: 0
}
}
},
methods: {
resetImportData() {
this.importData = {
fileList: [],
isFlag: false
}
this.errorInfoData = {
totalCount: 0,
successCount: 0,
errorCount: 0,
errorList: 0
}
},
doImport() {
if (this.importData.fileList.length === 0) {
this.$message.error({
title: "错误",
message: "请选择文件!"
})
return
}
const data = new FormData()
data.append("isFlag", this.importData.isFlag)
data.append("businessId", this.business_id)
this.importData.fileList.forEach((val) => {
data.append("file", val.raw, val.raw.name)
})
const loading = this.$loading({
lock: true,
text: "正在导入中请稍后...",
spinner: "el-icon-loading"
})
this.$axios.post(this.post_url, data).then((res) => {
loading.close()
if (res.code === 0) {
if (res.data) {
this.$message.warning("导入失败")
this.errorInfoData = res.data
this.$emit("flush")
} else {
this.$message.success("导入成功")
this.$emit("flush")
this.resetImportData()
}
} else {
this.$message.warning("导入失败")
}
})
},
exportErrors() {
const data = this.errorInfoData.errorList
// 创建工作簿
const workbook = XLSX.utils.book_new()
// 创建工作表
const worksheet = XLSX.utils.json_to_sheet(data)
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1")
// 将工作簿转换为二进制对象
const excelBuffer = XLSX.write(workbook, { bookType: "xlsx", type: "array" })
// 将二进制对象转换为Blob对象
const blob = new Blob([excelBuffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" })
// 创建下载链接并设置相关属性
const url = window.URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = "错误记录.xlsx"
// 模拟点击下载链接
document.body.appendChild(link)
link.click()
// 清理下载链接
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
},
fileHandleRemove(file, fileList) {
return fileList
},
fileHandleChange(file, fileList, { type, size }) {
const removeFile = () => {
fileList.splice(fileList.findIndex((v) => v === file))
}
if (!file.size) {
this.$message.warning("您选择的是空文件")
removeFile()
}
if (type && type.length && !type.includes(file.name.split(".").pop().toLowerCase())) {
this.$message.warning(`文件只能是 ${type.map((v) => v.toUpperCase()).join("/")} 格式`)
removeFile()
}
if (size && !file.size < size) {
this.$message.warning(`文件大小不能超过 ${size / 1024 / 1024}MB`)
removeFile()
}
return fileList
}
}
}
</script>
<style>
.el-card__body {
padding: 25px;
}
</style>
@@ -0,0 +1,16 @@
<template>
<div class="search">
<slot></slot>
<div class="search-query">
<el-button type="primary" icon="el-icon-search" @click="$emit('search', null)">搜索</el-button>
</div>
</div>
</template>
<script>
module.exports = {
name: "pageFormSearch"
}
</script>
<style scoped></style>
@@ -0,0 +1,24 @@
<template>
<div class="search-item">
<div class="search-item-label" v-if="label">
{{ label }}
</div>
<div class="search-item-option">
<slot></slot>
</div>
</div>
</template>
<script>
module.exports = {
name: "searchItem",
props: {
label: {
type: String,
default: ""
}
}
}
</script>
<style scoped></style>
@@ -0,0 +1,64 @@
<script>
module.exports = {
name: "index",
props: {
value: String,
/*最大高度,超过该高度显示折叠展开*/
height: Number
},
data() {
return {
isExpanded: false
}
},
methods: {
onView(e) {
if (e.target.tagName === "IMG") {
vant && vant.ImagePreview([e.target.src])
}
},
toggleDetails() {
this.isExpanded = !this.isExpanded
}
}
}
</script>
<template>
<div>
<div class="content" v-html="value" v-if="value" @click="onView" :style="{ maxHeight: isExpanded ? 'none' : height + 'px' }"></div>
<div v-if="value && height" class="toggle-button" @click="toggleDetails">
{{ isExpanded ? "收起" : "查看全部详情" }}
<van-icon :name="isExpanded ? 'arrow-up' : 'arrow-down'"></van-icon>
</div>
<van-empty v-if="!value" description="没有更多了"></van-empty>
</div>
</template>
<style scoped>
.content {
padding: 10px;
width: 100% !important;
max-width: 100% !important;
overflow: hidden;
word-break: break-word;
display: block;
box-sizing: border-box;
background: #fff;
transition: max-height 0.3s ease;
}
.content img {
width: 100% !important;
}
.toggle-button {
cursor: pointer;
text-align: center;
margin: 10px 0;
font-size: 14px;
padding: 5px 10px;
border-radius: 5px;
color: rgb(119 115 115);
}
</style>
@@ -0,0 +1,112 @@
<template>
<div class="h5-signature">
<div v-if="!showSignaturePanel" class="main-panel">
<slot v-if="$slots.default"></slot>
<template v-else>
<van-button @click="openSignature" class="open-button" size="middle" plain native-type="button">打开签字板</van-button>
<van-image :src="signatureContent"></van-image>
</template>
</div>
<van-popup v-model="showSignaturePanel" :style="{ height: '100vh', width: '100vw' }">
<signature @save="save"></signature>
</van-popup>
</div>
</template>
<script>
module.exports = {
name: "h5",
components: {
signature: httpVueLoader("/components/plugins/sysSignature/index.vue?v=" + new Date().getTime())
},
props: {
value: {
type: String,
default: ""
}
},
watch: {
value: {
handler(val) {
this.signatureContent = val
},
immediate: true
}
},
data() {
return {
signatureContent: null,
showSignaturePanel: false
}
},
methods: {
openSignature() {
this.showSignaturePanel = true
},
save(signature) {
const file = this.base64ToFile(signature, "signature.png")
const formData = new FormData()
formData.append("file", file)
axios
.post("/platform/signature/saveH5Signature", formData, {
headers: {
"Content-Type": "multipart/form-data"
}
})
.then((res) => {
return res.data
})
.then((res) => {
if (res.code === 0) {
alert("保存成功")
this.showSignaturePanel = false
this.$emit("input", res.data)
} else {
alert("保存失败")
}
})
},
base64ToFile(base64Data, filename) {
// 将base64的数据部分提取出来
const parts = base64Data.split(";base64,")
const contentType = parts[0].split(":")[1]
const raw = window.atob(parts[1])
const rawLength = raw.length
const uInt8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i)
}
// 使用Blob对象创建File对象
const blob = new Blob([uInt8Array], { type: contentType })
blob.lastModifiedDate = new Date()
blob.name = filename
return new File([blob], filename, { type: contentType })
}
},
created() {}
}
</script>
<style scoped>
.h5-signature {
width: 100%;
}
.main-panel {
position: relative;
flex-direction: column-reverse;
display: flex;
row-gap: 10px;
}
.main-panel .van-image {
min-height: 150px;
}
.main-panel .open-button {
color: var(--color-primary);
}
</style>
@@ -0,0 +1,153 @@
<template>
<div class="signature-wrap">
<canvas id="canvas"></canvas>
<div class="action-buttons">
<button @click="clear" class="action-button-danger" type="button">清空</button>
<button @click="undo" class="action-button-warning" type="button">撤销</button>
<button @click="save" class="action-button-primary" type="button">确定</button>
</div>
</div>
</template>
<script>
// function a() {
// // 检查当前屏幕方向
// if (window.orientation === 0 || window.orientation === 180) {
// // 竖屏状态
// console.log("竖屏状态")
// alert("当前不支持竖屏,请将设备调整为横屏!")
// } else {
// // 横屏状态,执行禁止旋转的操作
// alert("当前不支持横屏,请将设备调整为竖屏!")
// // 可以在这里使内容固定在竖屏方向
// const innerWidth = window.innerWidth
// const innerHeight = window.innerHeight
// alert(innerHeight)
// alert(innerWidth)
// }
// }
//
// window.addEventListener("orientationchange", a)
// a()
let signature = null
module.exports = {
name: "sysSignature",
data() {
return {
content: null
}
},
methods: {
clear() {
signature.clear()
},
undo() {
signature.undo()
},
rotate() {
signature.getRotateCanvas(90)
},
save() {
if (signature.isEmpty()) {
alert("请签字后再保存")
return
}
// const png = signature.getPNG()
//旋转一下 横过来
const rotateCanvas = signature.getRotateCanvas(-90)
this.$emit("save", rotateCanvas.toDataURL())
}
},
mounted() {
const innerWidth = window.innerWidth
const innerHeight = window.innerHeight
const canvas = document.getElementById("canvas")
canvas.width = window.innerWidth
canvas.height = window.innerHeight
signature = new SmoothSignature(canvas, {
width: innerWidth - 30,
height: innerHeight - 30,
scale: 2,
minWidth: 4,
maxWidth: 10,
color: "#000000"
// bgColor: "#f6f6f6"
})
}
}
</script>
<style scoped>
.signature-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 15px;
background: #ffffff;
}
.signature-wrap .action {
width: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.signature-wrap .action-buttons {
white-space: nowrap;
transform: rotate(90deg);
display: flex;
column-gap: 10px;
position: fixed;
bottom: 0px;
left: 60px;
transform: rotate(90deg);
display: flex;
flex-direction: column;
row-gap: 20px;
}
.action-buttons button {
color: #ffffff;
position: relative;
display: inline-block;
box-sizing: border-box;
height: auto;
padding: 3px 12px;
margin: 0;
font-size: 15px;
line-height: 1.4;
text-align: center;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.15s ease;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.action-button-primary {
background-color: #1677ff;
border: 1px solid #1677ff;
}
.action-button-danger {
background-color: #ff3141;
border: 1px solid #ff3141;
}
.action-button-warning {
background-color: #ff8f1f;
border: 1px solid #ff8f1f;
}
.signature-wrap canvas {
flex: 1;
border-radius: 10px;
border: 2px dashed #ccc;
}
</style>
@@ -0,0 +1,97 @@
<template>
<el-card shadow="never" class="pc-signature-card">
<div class="pc-signature">
<el-link type="primary">扫描下方二维码进行签字</el-link>
<div class="signature-body">
<QrCode v-if="!signatureContent" :options="{ width: 126 }" :value="signatureAddress" class="signature-qrcode"></QrCode>
<el-image v-if="signatureContent" :src="signatureContent" class="signature-image"></el-image>
</div>
<el-link type="danger" icon="el-icon-edit" @click="signatureAgain" v-if="signatureContent">重签</el-link>
</div>
</el-card>
</template>
<script>
module.exports = {
name: "sysSignaturePc",
store,
props: {
value: {
type: String,
default: ""
}
},
watch: {
value: {
handler(val) {
console.log(val)
if(val){
this.signatureContent = val
this.$emit("input", this.signatureContent)
}
},
immediate: true
}
},
data() {
return {
//前端唯一标识 当然后端还有用户id作为唯一标识
id: new Date().getTime() + Math.floor(Math.random() * 10000) + 1,
signatureAddress: null,
interval: null,
signatureContent: null
}
},
methods: {
async init() {
const { code, data } = await this.$axios.post("/platform/signature/get")
if (code === 0) {
this.signatureContent = data && data.signature
this.$emit("input", this.signatureContent)
}
this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id
console.log(this.signatureAddress)
},
signatureAgain() {
const val = sessionStorage.getItem("h5-scan-code-signature-" + this.id)
this.signatureContent = val
this.$emit("input", val)
}
},
mounted() {
this.init()
webSocketPubSub.subscribe("h5-scan-code-signature", (data) => {
console.info("ws:收到签字成功消息:", data)
this.signatureContent = data.value
this.$emit("input", data.value)
})
}
}
</script>
<style scoped>
.pc-signature-card {
border: 1px solid var(--border-color-base);
}
.pc-signature {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
row-gap: 20px;
}
.signature-body {
position: relative;
}
.signature-qrcode {
transition: opacity 0.5s ease;
}
.signature-image {
height: 100px;
width: auto;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div ref="svgContainer" class="svg-icon" :style="{ 'font-size': size + 'px' }"></div>
</template>
<script>
module.exports = {
name: "index",
props: {
name: {
type: String
},
size: {
type: Number,
default: 26
}
},
data() {
return {}
},
methods: {},
created() {
if (this.name) {
fetch("/assets/platform/img/svg/" + this.name + ".svg")
.then(async (res) => {
const svg = await res.text()
if (this.$refs.svgContainer) {
this.$refs.svgContainer.innerHTML = ""
this.$refs.svgContainer.innerHTML = svg
}
})
.catch(() => {})
}
}
}
</script>
<style scoped>
.svg-icon {
display: inline-flex;
font-size: 20px;
}
.svg-icon svg {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
@@ -0,0 +1,294 @@
<template>
<div class="ele-table-tool">
<div class="ele-table-tool-title">
<div class="ele-table-tool-title-label">
<slot v-if="$slots.label" name="label"></slot>
<span v-else>{{ label }}</span>
</div>
<div class="ele-table-tool-title-content">
<slot v-if="$slots.content" name="content"></slot>
<span v-else>{{ content }}</span>
</div>
</div>
<div class="ele-tool">
<div class="ele-space">
<slot></slot>
<!-- <el-radio-group v-if="show_audit" v-model="auditState" @change="auditStateChange" size="small">-->
<!--&lt;!&ndash; <el-radio-button :label="null">全部</el-radio-button>&ndash;&gt;-->
<!-- <el-radio-button :label="true">已审核</el-radio-button>-->
<!-- <el-radio-button :label="false">未审核</el-radio-button>-->
<!-- </el-radio-group>-->
</div>
<!-- <div class="ele-tool-item ele-action"></div>-->
</div>
</div>
</template>
<script>
module.exports = {
name: "index",
props: {
label: {
type: String,
default: "表格数据"
},
content: {
type: String,
default: ""
},
show_audit: {
type: Boolean,
default: false
},
audit_state: {
type: Boolean,
default: null
}
},
data() {
return {
auditState: false
}
},
methods: {
auditStateChange(val) {
if (this.show_audit) {
this.$emit("update:audit_state", val)
this.$emit("search", null)
}
}
},
watch: {
audit_state: {
handler: function (val) {
this.auditState = val === null ? false : val
},
immediate: true
}
},
created() {}
}
</script>
<style scoped>
.ele-table-tool:not(.ele-table-tool-default):not(.ele-table-tools-none) .ele-tool .ele-tool-item {
padding: 6px;
line-height: 1;
text-align: center;
border: unset;
border-radius: unset;
font-size: 14px;
}
.ele-table-tool .ele-table-tool-title {
margin: 0;
}
.ele-table-tool-title-label {
position: relative;
padding-left: 1em;
color: var(--color-primary);
font-weight: bold;
}
.ele-table-tool-title-label::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 5px;
height: 1.3em;
background-color: var(--color-primary);
border-radius: 2px;
}
.ele-table-tool {
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-ms-flex-align: center;
margin-bottom: 10px;
}
.ele-table-tool,
.ele-table-tool .ele-table-tool-title {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
align-items: center;
}
.ele-table-tool .ele-table-tool-title {
-webkit-box-flex: 1;
-ms-flex: auto;
flex: auto;
margin-top: 5px;
margin-bottom: 5px;
-ms-flex-align: center;
}
.ele-table-tool .ele-table-tool-title > .ele-table-tool-title-label {
margin-right: 8px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.ele-table-tool .ele-table-tool-title > .ele-table-tool-title-content {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.ele-table-tool .ele-tool {
margin: 5px 0 5px auto;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.ele-table-tool .ele-tool .ele-tool-item {
font-size: 18px;
padding: 0 2px;
cursor: pointer;
}
.ele-table-tool .ele-tool .ele-tool-item .el-dropdown > i {
font-size: 18px;
}
.ele-table-tool .ele-tool .ele-tool-item + .ele-tool-item {
margin-left: 10px;
}
.ele-table-tool.ele-toolbar-form .ele-table-tool-title {
margin-top: 0;
margin-bottom: 0;
}
.ele-table-tool.ele-toolbar-form .ele-table-tool-title .el-form-item,
.ele-table-tool.ele-toolbar-form .ele-table-tool-title .ele-form-actions {
margin-top: 5px;
margin-bottom: 5px;
}
.ele-table-tool.ele-toolbar-actions .ele-table-tool-title {
margin-top: 0;
margin-bottom: 0;
}
.ele-table-tool.ele-toolbar-actions .ele-table-tool-title > .el-button,
.ele-table-tool.ele-toolbar-actions .ele-table-tool-title > .el-dropdown,
.ele-table-tool.ele-toolbar-actions .ele-table-tool-title > .el-link,
.ele-table-tool.ele-toolbar-actions .ele-table-tool-title > .el-tag,
.ele-table-tool.ele-toolbar-actions .ele-table-tool-title > .ele-action {
margin-top: 5px;
margin-bottom: 5px;
}
.ele-table-tool:not(.ele-table-tool-default):not(.ele-table-tools-none) .ele-tool .ele-tool-item {
padding: 6px;
line-height: 1;
text-align: center;
border: 1px solid var(--border-color-base);
border-radius: 50%;
font-size: 14px;
}
.ele-table-tool:not(.ele-table-tool-default):not(.ele-table-tools-none) .ele-tool .ele-tool-item .el-dropdown > i {
font-size: 14px;
}
.ele-table-tool:not(.ele-table-tool-default):not(.ele-table-tools-none) .ele-tool .ele-tool-item:hover {
color: var(--color-primary);
border-color: var(--color-primary-3);
background-color: var(--color-primary-1);
}
.ele-table-tool:not(.ele-table-tool-default):not(.ele-table-tools-none) .ele-tool .ele-tool-item:hover .el-dropdown > i {
color: var(--color-primary);
}
.ele-table-tool-default {
margin-bottom: 0;
padding: 5px 15px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
background: var(--table-header-background-color);
border-top: 1px solid var(--border-color-lighter);
border-left: 1px solid var(--border-color-lighter);
border-right: 1px solid var(--border-color-lighter);
}
.ele-table-tool-default .ele-tool .ele-tool-item {
font-size: 16px;
padding: 5px 6px;
border-radius: 2px;
border: 1px solid var(--border-color-light);
-webkit-box-sizing: border-box;
box-sizing: border-box;
line-height: 1;
}
.ele-table-tool-default .ele-tool .ele-tool-item .el-dropdown > i {
font-size: 16px;
}
.ele-tab-tool {
padding: 0 15px;
-ms-flex-negative: 0;
flex-shrink: 0;
width: auto;
min-width: 40px;
height: 40px;
line-height: 40px;
-webkit-transition:
background-color 0.3s,
color 0.3s;
transition:
background-color 0.3s,
color 0.3s;
-webkit-box-sizing: border-box;
box-sizing: border-box;
text-align: center;
position: relative;
cursor: pointer;
}
.ele-tab-tool .el-icon-house {
font-size: 16px;
vertical-align: -1px;
}
.ele-tab-tool.is-tab:hover {
color: var(--color-primary);
background: var(--header-tool-hover-bg);
}
.ele-tab-tool.is-tab:after {
content: "";
width: 0;
height: 2px;
background: var(--color-primary);
position: absolute;
bottom: 0;
left: 0;
}
.ele-tab-tool.is-tab.is-active {
color: var(--color-primary);
background: var(--color-primary-1);
}
.ele-tab-tool.is-tab.is-active:after {
width: 100%;
}
</style>
@@ -0,0 +1,223 @@
<template>
<div>
<div ref="editorRef"></div>
<input type="file" style="display: none" />
</div>
</template>
<script>
const { $, BtnMenu, Panel, Tooltip } = wangEditor
//注册上传word按钮
class DocMenu extends BtnMenu {
constructor(editor) {
const $elem = wangEditor.$(
`<div class="w-e-menu" data-title="上传文档">
<i class="fa fa-file-word-o" aria-hidden="true" /></i>
</div>`
)
super($elem, editor)
}
clickHandler() {
const fileInputId = this.editor.textElemId + "input-file"
const existingInput = document.getElementById(fileInputId)
if (existingInput) {
existingInput.remove()
}
const input = document.createElement("input")
input.type = "file"
input.accept = "application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
input.id = fileInputId
input.style.display = "none"
input.addEventListener("change", (event) => {
const files = event.target.files
if (!files || files.length === 0) {
return
}
let loading = null
if (window.ELEMENT) {
loading = ELEMENT.Loading.service({
lock: true,
text: "文件读取中",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)"
})
} else if (window.vant) {
loading = vant.Toast.loading({
message: "文件读取中",
forbidClick: true,
overlay: true,
duration: 0
})
}
const formData = new FormData()
formData.append("file", files[0])
axios
.post("/platform/sys/file/convertHtml", formData, {
headers: { "Content-Type": "multipart/form-data", "x-requested-with": "XMLHttpRequest" }
})
.then((response) => {
loading.close()
if (response.data.code === 0) {
this.editor.txt.html(response.data.data)
} else {
if (window.ELEMENT) {
this.$message.error(response.data.msg)
} else if (window.vant) {
vant.Toast(response.data.msg)
} else {
alert(response.data.msg)
}
}
})
.catch((error) => {
console.log(error)
alert("上传word失败,请联系管理员!")
})
.finally(() => {
input.remove()
})
})
input.click()
document.body.appendChild(input)
}
tryChangeActive() {}
}
wangEditor.registerMenu("docMenu", DocMenu)
module.exports = {
name: "textEditor",
props: {
content: {
type: String,
default: ""
},
// 定义值属性,用于接收父组件传递的值
value: {
type: String,
default: ""
},
height: {
type: Number,
default: 300
},
menus: {
type: Array,
default: () => {
return [
"head", //标题
"bold", //加粗
"fontSize", //字号
"fontName", //字体
"italic", //斜体
"underline", //下划线
"strikeThrough", //删除线
"indent", //缩进
"lineHeight", //行高
"foreColor", //文字颜色
"backColor", //背景颜色
//'link',//链接
"list", //序列
"todo", //待办
"justify", //对齐
"quote", //引用
// 'emoticon',//表情
"image",
//'video',
"table",
//'code',
"splitLine", //分割线
"undo",
"redo"
]
}
}
},
data() {
return {
editor: null,
cursorPos: 0
}
},
mounted() {
this.initEditor()
},
watch: {
value: {
handler: function (newValue) {
this.$nextTick(() => {
const content = newValue || ""
if (this.editor.txt.html() !== content) {
const selection = this.editor.selection
const range = selection.getRange() // 保存当前选区
this.editor.txt.html(content)
// 如果之前有选区,则恢复光标位置
if (range) {
selection.restoreSelection()
}
}
})
},
immediate: true
}
},
methods: {
initEditor() {
let _this = this
this.editor = new wangEditor(this.$refs.editorRef)
this.editor.config.focus = false
//内容change回调
this.editor.config.onchange = (newHtml) => {
this.cursorPos = this.editor.selection.getCursorPos()
const range = this.editor.selection.getRange()
range.setStart(range.startContainer, Math.max(this.cursorPos, range.endOffset))
this.$emit("input", newHtml)
}
this.editor.config.linkImgCheck = function (imgSrc, alt, href) {
// 1. 返回 true ,说明检查通过
return true
}
//配置图片上传
this.editor.config.customUploadImg = function (resultFiles, insertImgFn) {
// resultFiles 是 input 中选中的文件列表
// insertImgFn 是获取图片 url 后,插入到编辑器的方法
Promise.all(_this.uploadFiles(resultFiles)).then((res) => {
res.forEach((v) => {
if (v.data && v.data.code === 0) {
insertImgFn(v.data.data)
} else {
this.$message.error("图片上传失败")
}
})
})
}
this.editor.config.pasteFilterStyle = false
this.editor.config.height = this.height
this.editor.config.menus = this.menus
this.editor.create()
},
uploadFiles(resultFiles) {
return resultFiles.map(async (file) => {
const formData = new FormData()
formData.append("file", file)
return axios.post("/platform/sys/file/uploadDynamicReturnUrl", formData, {
headers: {
"Content-Type": "multipart/form-data"
}
})
})
}
},
created() {}
}
</script>
<style scoped></style>
@@ -0,0 +1,189 @@
<template>
<div>
<van-uploader
v-model="fileList"
:before-read="beforeRead"
:after-read="afterRead"
multiple
progress
:accept="accept"
:max-count="upload_number"
title=""
description=""
tip=""
></van-uploader>
</div>
</template>
<script>
module.exports = {
name: "h5Index",
props: {
// 上传返回id
upload_return_id_api: {
type: String,
default: "/platform/sys/file/uploadLocalReturnId",
required: false
},
// 上传返回url
upload_dynamic_return_url_api: {
type: String,
default: "/platform/sys/file/uploadDynamicReturnUrl",
required: false
},
// 当上传接口为id的情况下,配置下载接口
upload_id_download_url: {
type: String,
default: "/platform/sys/file/download?id=",
required: false
},
// 上传样式或图片方式 file || drag || image
upload_mode: {
type: String,
default: "file",
required: false
},
// 上传数量
upload_number: {
type: Number,
default: 1,
required: false
},
// 上传返回id或url
upload_result_type: {
type: String,
default: "url",
required: false
},
// 上传返回分类 数组或字符串逗号隔离 interval | array
upload_result_category: {
type: String,
default: "interval",
required: false
},
// 跟antdv官方一样,是否显示文件列表
show_upload_list: {
type: Boolean,
default: true,
required: false
},
// 跟antdv官方一样,接受上传的文件类型
accept: {
type: String,
default: "",
required: false
},
// 是否是完整的结果(就是文件上传返回什么,该组件返回什么,uploadResultCategory必须为array
complete_result: {
type: Boolean,
default: false,
required: false
},
// 父组件传来的参数
value: {
type: [String, Array],
required: false
}
},
data() {
return {
fileList: [
// { url: "http://localhost:8080/platform/sys/file/download?id=rn1v1efh2ag0aps59ult3ebudf", isImage: true }
]
}
},
watch: {
value: {
handler: function (val) {
if (val) {
if (Array.isArray(val)) {
this.fileList = val.map((v) => {
return {
...v,
status: null,
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase())
}
})
} else {
this.fileList = val.map((v) => {
return {
...v,
url: v.url ? v.url : v.response?.data,
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()),
status: null
}
})
}
} else {
this.fileList = []
}
console.log(this.fileList)
},
immediate: true
}
},
computed: {
action() {
return this.upload_result_type === "id" ? this.upload_return_id_api : this.upload_dynamic_return_url_api
}
},
methods: {
beforeRead(file) {
return true
},
afterRead(files) {
const uploadPromises = this.fileList
.filter((f) => f.status === "loading")
.map((f) => {
const formData = new FormData()
formData.append("file", f.file)
return this.$axios.post(this.action, formData).then((resp) => {
if (resp.code === 0) {
console.log(resp)
f.name = f.file.name
f.size = f.file.size
f.status = null
f.url = resp.data
f.response = resp
f.percentage = 100
f.isImage = true
delete f.file
delete f.content
console.log(f)
} else {
f.status = "fail"
}
})
})
Promise.all(uploadPromises)
.then(() => {
if (this.upload_result_category === "interval") {
} else if (this.upload_result_category === "array") {
if (this.complete_result) {
this.$emit("update:value", this.fileList)
} else {
const resultArrayValue = []
this.fileList.forEach((data) => {
resultArrayValue.push(data.response.data)
})
this.$emit("update:value", resultArrayValue)
}
}
})
.catch((error) => {
console.error("所有上传请求失败:", error)
})
}
},
created() {}
}
</script>
<style scoped>
.van-uploader__title {
padding-left: 0;
}
.van-uploader__wrapper {
margin-left: 0;
}
</style>
@@ -0,0 +1,585 @@
<template>
<div class="upload_file">
<el-upload
v-if="upload_mode === 'file'"
:file-list="fileList"
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-preview="handlePreview"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
:limit="upload_number"
:accept="fileAccept"
>
<el-button :size="upload_button_size" type="primary">
{{ upload_text }}
</el-button>
</el-upload>
<!-- 图片上传模式 -->
<el-upload
v-if="upload_mode === 'image'"
:file-list="fileList"
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
:limit="upload_number"
:accept="fileAccept"
list-type="picture-card"
>
<i class="el-icon-plus"></i>
<!-- <div class="el-upload__tip" v-if="fileList.length < upload_number">-->
<!-- <div style="margin-top: 8px">{{ upload_text }}</div>-->
<!-- </div>-->
</el-upload>
<!-- 拖拽上传模式 -->
<template v-if="upload_mode === 'drag'">
<el-tabs class="mb10" v-model="uploadMode">
<el-tab-pane label="电脑选择文件" name="pc">
<span slot="label">
<i class="fa fa-desktop"></i>
电脑选择文件
</span>
</el-tab-pane>
<el-tab-pane label="手机扫码上传" name="mobile">
<span slot="label">
<i class="fa fa-qrcode"></i>
手机扫码上传
</span>
</el-tab-pane>
</el-tabs>
<el-upload
ref="dragUploadRef"
drag
:file-list="fileList"
:multiple="true"
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
:limit="upload_number"
:accept="fileAccept"
class="drag-upload"
:class="{ 'drag-upload-disabled': uploadMode !== 'pc' }"
>
<div slot="trigger" style="height: 134px" v-if="uploadMode === 'pc'">
<i class="el-icon-upload"></i>
<div class="el-upload__text">{{ upload_text }}</div>
</div>
<div>
<div class="h5-qrcode" v-if="uploadMode === 'mobile'">
<QrCode :options="{ width: 126 }" :value="qrCodeAddress" class="signature-qrcode"></QrCode>
</div>
</div>
<div class="el-upload__tip" v-html="upload_tips"></div>
</el-upload>
</template>
<!-- 插槽用于显示额外的说明信息 -->
<slot name="explain"></slot>
</div>
</template>
<script>
module.exports = {
props: {
// 上传返回id
upload_return_id_api: {
type: String,
default: "/platform/sys/file/uploadLocalReturnId",
required: false
},
// 上传返回url
upload_dynamic_return_url_api: {
type: String,
default: "/platform/sys/file/uploadDynamicReturnUrl",
required: false
},
// 当上传接口为id的情况下,配置下载接口
upload_id_download_url: {
type: String,
default: "/platform/sys/file/download?id=",
required: false
},
// 上传样式或图片方式 file || drag || image
upload_mode: {
type: String,
default: "file",
required: false
},
// 上传数量
upload_number: {
type: Number,
default: 1,
required: false
},
// 上传按钮文字
upload_text: {
type: String,
default: "点击上传文件",
required: false
},
// 上传按钮大小
upload_button_size: {
type: String,
default: "small",
required: false
},
// 上传返回id或url
upload_result_type: {
type: String,
default: "url",
required: false
},
// 上传返回分类 数组或字符串逗号隔离 interval | array
upload_result_category: {
type: String,
default: "interval",
required: false
},
// 是否显示文件列表
show_upload_list: {
type: Boolean,
default: true,
required: false
},
// 接受上传的文件类型
accept: {
type: String,
default: "",
required: false
},
// 是否是完整的结果(就是文件上传返回什么,该组件返回什么,uploadResultCategory必须为array
complete_result: {
type: Boolean,
default: false,
required: false
},
// 父组件传来的参数
value: {
type: [String, Array],
default: undefined,
required: false
}
},
data() {
return {
fileList: [],
previewVisible: false,
previewImage: "",
fileAccept: null,
dragDisabled: true,
qrCodeAddress: origin + "/platform/sys/h5ScanCodeUploadFile",
uploadMode: "pc"
}
},
computed: {
action() {
return this.upload_result_type === "id" ? this.upload_return_id_api : this.upload_dynamic_return_url_api
},
upload_tips() {
const uploadNumber = this.upload_number
const fileAccept = this.fileAccept
let tips = null
if (uploadNumber) {
tips = `只能上传<span style="color: red">${uploadNumber}</span>个文件;`
}
if (fileAccept) {
tips = tips + `只能上传<span style="color: red">${fileAccept}</span>文件;`
}
return tips
}
},
methods: {
buildFileObject(url, id) {
return {
data: url ? url : this.upload_id_download_url + id,
name: url ? url : id,
url: url ? url : this.upload_id_download_url + id,
status: "success",
response: {
data: url ? url : id,
code: 200
}
}
},
echo(newVal) {
// 字符串隔离情况
if (this.upload_result_category === "interval") {
// id隔离
if (this.upload_result_type === "id") {
newVal.split(",").forEach((id) => {
const file = this.buildFileObject(undefined, id)
this.fileList.push(file)
this.fileList.reverse()
})
}
// url隔离
if (this.upload_result_type === "url") {
newVal.split(",").forEach((url) => {
const file = this.buildFileObject(url)
this.fileList.push(file)
this.fileList.reverse()
})
}
}
// 如果是数组的情况下
if (this.upload_result_category === "array") {
if (this.complete_result) {
// 得去掉数组里面的thumbUrl,一个base64太大,无用
// let newResult = cloneDeep(newVal)
let newResult = JSON.parse(JSON.stringify(newVal))
newResult.map((e) => {
if (e.thumbUrl) {
delete e.thumbUrl
}
if (this.upload_result_type === "id") {
e.url = this.upload_id_download_url + e.response.data
}
if (this.upload_result_type === "url") {
e.url = e.response.data
}
})
this.fileList = newResult
} else {
// id数组
if (this.upload_result_type === "id") {
if (Array.isArray(newVal)) {
newVal.forEach((id) => {
this.fileList.push(this.buildFileObject(undefined, id))
})
} else {
try {
JSON.parse(newVal).forEach((id) => {
this.fileList.push(this.buildFileObject(undefined, id))
})
} catch (e) {
console.error(e)
}
}
}
// url数组
if (this.upload_result_type === "url") {
if (Array.isArray(newVal)) {
newVal.forEach((url) => {
this.fileList.push(this.buildFileObject(url))
})
} else {
try {
JSON.parse(newVal).forEach((url) => {
this.fileList.push(this.buildFileObject(url))
})
} catch (e) {
console.error(e)
}
}
}
}
}
},
// 这是兜底逻辑,保准只让这个image类型上传图片
beforeUpload(file) {
return true
// if (!this.fileAccept || typeof this.fileAccept !== "string") {
// return true
// }
//
// // 去除前后空格并转换为数组
// const acceptTypes = this.fileAccept
// .trim()
// .split(",")
// .map((type) => type.trim().toUpperCase())
//
// let fileType
// const dotIndex = file.name.lastIndexOf(".")
// if (dotIndex !== -1) {
// fileType = file.name.substring(dotIndex).toUpperCase()
// } else {
// this.$message.error("文件必须包含有效的扩展名!")
// return false
// }
//
// const valid = acceptTypes.includes(fileType)
// if (valid) {
// return true
// }
// this.$message.error(`文件只能是 ${this.fileAccept} 格式!`)
// return false
},
// 预览图片
handlePreview(file) {
const suffix = file.name.substring(file.name.lastIndexOf(".") + 1).toLowerCase()
const fileId = file.response.data.substring(file.response.data.lastIndexOf("=") + 1)
const buildFile = {
id: fileId,
suffix,
name: file.name,
downloadPath: file.response.data
}
this.$commonUtil.previewFile(buildFile)
},
handleRemove(file, fileList) {
if (this.upload_result_category === "interval") {
if (this.upload_result_type === "id") {
}
if (this.upload_result_type === "url") {
}
}
if (this.upload_result_category === "array") {
if (this.complete_result) {
this.$emit("update:value", fileList)
} else {
if (this.upload_result_type === "id") {
this.$emit(
"update:value",
fileList.map((e) => e.response.data)
)
} else {
this.$emit(
"update:value",
fileList.map((e) => e.response.data)
)
}
}
}
},
handleExceed(files, fileList) {
this.$message.warning("最多只能上传" + this.upload_number + "个文件")
},
// 上传事件
handleChange(file, fileList) {
//
// let result = []
// // const file = uploads.file
// debugger
// if (file && (file.status === 'done' || file.status === 'removed') && file.response && file.response.code === 200) {
// uploads.fileList.forEach((f) => {
// result.push(f)
// })
// }
// if (result.length > 0) {
// if (this.upload_result_category === 'interval') {
// let resultIntervalValue = ''
// result.forEach((data) => {
// resultIntervalValue =
// data.response.data + (resultIntervalValue ? ',' + resultIntervalValue : '')
// })
// this.$emit('update:value', resultIntervalValue)
// this.$emit('onChange', resultIntervalValue)
// } else if (this.upload_result_category === 'array') {
// if (this.complete_result) {
// // 得去掉数组里面的thumbUrl,一个base64太大,无用
// let newResult = JSON.parse(JSON.stringify(newResult))
// newResult.map((e) => {
// if (e.thumbUrl) {
// delete e.thumbUrl
// }
// })
// this.emit('update:value', newResult)
// this.emit('onChange', newResult)
// } else {
// const resultArrayValue = []
// result.forEach((data) => {
// resultArrayValue.push(data.response.data)
// })
// this.emit('update:value', resultArrayValue)
// this.emit('onChange', resultArrayValue)
// }
// }
// return
// }
// this.$emit('update:value', undefined)
// this.$emit('onChange', undefined)
},
handleSuccess(response, file, fileList) {
let result = []
if (response && response.code === 0 && file.status === "success") {
fileList.forEach((f) => {
result.push(f)
})
} else {
this.$message.error(response.msg)
}
if (result.length > 0) {
if (this.upload_result_category === "interval") {
const resultArrayValue = []
result.forEach((data) => {
resultArrayValue.push(data.response.data)
})
this.$emit("update:value", resultArrayValue.join(","))
} else if (this.upload_result_category === "array") {
if (this.complete_result) {
// 得去掉数组里面的thumbUrl,一个base64太大,无用
let newResult = JSON.parse(JSON.stringify(result))
newResult.map((e) => {
if (!e.url) {
e.url = e.response.data
}
if (e.thumbUrl) {
delete e.thumbUrl
}
})
this.$emit("update:value", newResult)
} else {
const resultArrayValue = []
result.forEach((data) => {
resultArrayValue.push(data.response.data)
})
this.$emit("update:value", resultArrayValue)
}
}
return
}
this.$emit("update:value", null)
},
// 通过DOM获取上传的文件
uploadFileList() {
if (this.fileList) {
const result = []
// 只返回这些就够用了,其他基本用不到
this.fileList.value.forEach((item) => {
const obj = {
name: item.name,
type: item.type,
size: item.size,
url: item.response.data
}
result.push(obj)
})
return result
} else {
return []
}
}
},
watch: {
value: {
handler: function (newVal) {
if (this.value && newVal) {
this.fileList = []
this.echo(newVal)
} else {
this.fileList = []
this.$emit("update:value", undefined)
}
},
immediate: true,
deep: true
},
upload_mode: {
handler: function (newVal) {
if (newVal && newVal === "image") {
if (this.accept) {
this.fileAccept = this.accept
} else {
this.fileAccept = "image/*"
}
} else {
this.fileAccept = this.accept
}
},
immediate: true,
deep: true
},
accept: {
handler: function (newVal) {
if (newVal) {
this.fileAccept = newVal
} else {
this.fileAccept = this.accept
}
}
}
},
mounted() {
webSocketPubSub.subscribe("h5-scan-code-upload-file", (data) => {
console.log("ws:收到手机上传文件消息:", data)
const originFileList = this.fileList || []
console.log(data.files)
if (this.upload_number) {
if (originFileList.length + data.files.length > this.upload_number) {
this.$message.error("上传文件数量超出限制")
return
}
}
if (this.fileAccept) {
// 去除前后空格并转换为数组
const acceptTypes = this.fileAccept
.trim()
.split(",")
.map((type) => type.trim().toUpperCase())
const validFiles = data.files.filter((file) => {
let fileType
const dotIndex = file.name.lastIndexOf(".")
if (dotIndex !== -1) {
fileType = file.name.substring(dotIndex).toUpperCase()
}
return acceptTypes.includes(fileType)
})
if (validFiles.length < data.files.length) {
data.files = validFiles
this.$message.error(`文件只能是 ${this.fileAccept} 格式,已自动过滤掉不符合要求文件!`)
}
}
this.$message.success("获取手机文件成功")
const newFileList = originFileList.concat(data.files)
this.$emit("update:value", newFileList)
})
}
}
</script>
<style scoped>
.upload_file {
width: 100%;
}
.upload_file .el-upload {
/*width: 100%;*/
}
.upload_file .el-upload .el-upload-dragger {
width: 100%;
line-height: 26px;
height: auto;
border: 1px solid var(--border-color-base);
}
.el-upload-dragger .el-icon-upload {
margin: 10px 0 16px;
}
.drag-upload .el-upload {
width: 100%;
}
.upload_file .drag-upload-disabled > .el-upload {
pointer-events: none;
}
</style>
@@ -0,0 +1,146 @@
<template>
<div class="user-audit-opinion-textarea">
<el-input type="textarea" :rows="3" v-model="content" v-bind="$attrs" @change="onChange" @blur="onBlur"></el-input>
<div class="opinion-wrap">
<div class="title">常用审核意见</div>
<el-select v-model="selectOpinion" placeholder="可选择常用审核意见" size="small" @change="selectOpinionChange">
<el-option v-for="item in opinionOptions" :key="item.id" :label="item.text" :value="item.text"></el-option>
</el-select>
<div class="settings">
<el-link type="primary" @click="openSetting">自定义</el-link>
</div>
</div>
<el-dialog title="设置常用意见" :visible.sync="dialogVisible" width="50%" append-to-body>
<el-table :data="tableOpinionData" key="id">
<el-table-column label="序号" type="index" width="50"></el-table-column>
<el-table-column label="意见内容">
<template slot-scope="scope">
<el-input v-model="scope.row.text" placeholder="请输入内容" max="50"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="100px">
<template #header>
<el-button type="primary" size="mini" @click="pushNewOpinion">添加</el-button>
</template>
<template slot-scope="scope">
<el-button type="danger" size="mini" @click="tableOpinionData.splice(scope.$index, 1)">删除</el-button>
</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="updateOpinion">提交</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
module.exports = {
name: "index",
props: {
value: String
},
data() {
return {
content: null,
tableOpinionData: [],
opinionOptions: [],
selectOpinion: null,
dialogVisible: false
}
},
watch: {
value: {
handler: function (val) {
this.content = val
},
immediate: true
}
},
methods: {
onChange(val) {
this.$emit("input", val)
},
onBlur(val) {
this.$emit("blur", this.content)
},
selectOpinionChange(val) {
this.$emit("input", val)
},
openSetting() {
this.dialogVisible = true
this.listOpinion()
},
pushNewOpinion() {
if (this.tableOpinionData.length === 10) {
this.$message.error("最多可添加10条审批意见")
return
}
const id = Math.max(...this.tableOpinionData.map((v) => v.id), 0)
this.tableOpinionData.push({ id: id + 1, text: "" })
},
updateOpinion() {
this.$axios
.post("/platform/sys/userApproval/opinion/update", {
opinions: JSON.stringify(this.tableOpinionData)
})
.then((res) => {
if (res.code === 0) {
this.$message.success("设置成功")
this.dialogVisible = false
}
})
},
listOpinion() {
this.$axios.get("/platform/sys/userApproval/opinion/list").then((res) => {
if (res.code === 0) {
this.tableOpinionData = res.data || []
this.opinionOptions = res.data || []
}
})
}
},
created() {
this.listOpinion()
}
}
</script>
<style scoped>
.user-audit-opinion-textarea {
border: 1px solid var(--border-color-base);
border-radius: 4px;
}
.user-audit-opinion-textarea:focus {
border-color: var(--color-primary);
}
.user-audit-opinion-textarea textarea {
border: none;
}
.user-audit-opinion-textarea:focus {
background-color: transparent;
}
.user-audit-opinion-textarea .opinion-wrap .title {
font-weight: 500;
color: #000000;
}
.opinion-wrap {
padding: 5px 10px;
display: flex;
align-items: center;
justify-content: end;
}
.opinion-wrap .settings {
text-align: right;
margin-left: 5px;
}
</style>
@@ -0,0 +1,122 @@
<template>
<el-select v-model="selectValue" :clearable="clearable" filterable remote :remote-method="selectUser"
@change="onChange" v-bind="$attrs">
<el-option
v-for="item in options"
v-if="!item.disabled"
:key="item[option_value]"
:label="option_label_func ? option_label_func(item) : item[option_label]"
:value="item[option_value]"
></el-option>
</el-select>
</template>
<script>
module.exports = {
model: {
prop: "value",
event: "change"
},
props: {
value: {type: [String, Array]},
api: {
type: String,
required: false,
default: "/open/common/userOptions"
},
api_params: {
type: Object,
required: false,
default: () => {
return {}
}
},
clearable: {
type: Boolean,
default: true
},
api_input_key_name: {
type: String,
required: false,
default: "keyWord"
},
option_list: {
type: Array,
default: () => {
return []
}
},
option_value: {
type: String,
default: "id"
},
option_label: {
type: String,
default: "username"
},
option_label_func: {
type: Function,
required: false
}
},
data() {
return {
options: [],
selectValue: null
}
},
watch: {
value: {
handler: function (val) {
if (this.$attrs.multiple) {
this.selectValue = [...val]
} else {
this.selectValue = val ? val.toString() : ""
}
},
immediate: true,
deep: true
},
option_list: {
handler: function (val) {
if (val) {
this.options = val
}
},
immediate: true
}
},
methods: {
selectUser(query) {
if (query !== "") {
$.get(this.api, {[this.api_input_key_name]: query, ...this.api_params}).then((res) => {
this.options = res.data
})
}
},
onChange(val) {
this.$emit("change", val)
},
clearOptions() {
this.options = []
}
},
created() {
this.clearOptions()
}
}
</script>
<style></style>
@@ -0,0 +1,176 @@
<template>
<van-popup :round="true" :safe-area-inset-bottom="false" :close-on-click-overlay="ableClose" v-model="show">
<div class="van-text-dialog__wrapper">
<!-- 关闭图标 -->
<van-icon v-if="ableClose" role="button" tabindex="0" name="cross" class="van-text-dialog__close-icon" @click="close"></van-icon>
<!-- 标题 -->
<div class="van-text-dialog__title">
<slot name="title">{{ title }}</slot>
</div>
<!-- 内容主体 -->
<div class="van-text-dialog__body">
<template v-if="$slots.body">
<slot name="body"></slot>
</template>
<template v-else>
<div v-if="typeof body === 'string'" class="van-text-dialog__content">
{{ body }}
</div>
<template v-else-if="Array.isArray(body)">
<div v-for="(item, index) in body" :key="index" class="van-text-dialog__part">
<div v-if="item.title" class="van-text-dialog__subtitle">
{{ item.title }}
</div>
<div class="van-text-dialog__content">
{{ item.content }}
</div>
</div>
</template>
</template>
</div>
<!-- 底部线 & 遮罩 -->
<div v-if="mask" class="van-text-dialog__line van-hairline--bottom">
<div class="van-text-dialog__mask"></div>
</div>
<!-- 复选框 -->
<div v-if="checkboxLabel || $slots.checkboxLabel" class="van-text-dialog__checkbox">
<van-checkbox plain :icon-size="16" :disabled="!ableCheck" v-model="proxyCheck">
<slot name="checkbox-label">{{ checkboxLabel }}</slot>
</van-checkbox>
</div>
<!-- 确认按钮 -->
<van-button v-if="buttonText" type="primary" :disabled="!ableConfirm" block class="van-text-dialog__button" @click="confirm">
{{ buttonText }}
</van-button>
<!-- 默认插槽 -->
<slot></slot>
</div>
</van-popup>
</template>
<script>
module.exports = {
name: "vantTextDialog",
props: {
show: Boolean,
title: String,
body: [Array, String],
mask: Boolean,
check: {
type: Boolean,
default: true
},
checkboxLabel: String,
buttonText: String,
ableClose: {
type: Boolean,
default: true
},
ableCheck: {
type: Boolean,
default: true
},
ableConfirm: {
type: Boolean,
default: true
}
},
computed: {
proxyCheck: {
get() {
return this.check
},
set(val) {
this.$emit("toggle", val)
}
}
},
methods: {
close() {
this.$emit("close")
},
confirm() {
this.$emit("confirm")
}
}
}
</script>
<style scoped>
.van-text-dialog__wrapper {
min-width: 300px;
background: #fff;
padding: 18px 20px 20px;
}
.van-text-dialog__close-icon {
position: absolute;
top: 12px;
right: 12px;
font-size: 14px;
color: #999;
}
.van-text-dialog__title {
margin: 0 14px;
line-height: 25px;
text-align: center;
font-size: 18px;
color: #111;
font-weight: 700;
}
.van-text-dialog__body {
margin-top: 18px;
position: relative;
overflow-y: scroll;
max-height: 350px;
}
.van-text-dialog__part {
margin-bottom: 16px;
}
.van-text-dialog__subtitle {
margin-bottom: 4px;
line-height: 24px;
font-size: 14px;
font-weight: 700;
color: #111;
}
.van-text-dialog__content {
margin-bottom: 16px;
line-height: 24px;
font-size: 14px;
color: #333;
}
.van-text-dialog__line {
margin-top: 1px;
position: relative;
}
.van-text-dialog__line::after {
border-color: #e2e2e2;
}
.van-text-dialog__mask {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 58px;
z-index: 999;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.2) 50%, #fff 100%);
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.2) 50%, #fff 100%);
}
.van-text-dialog__checkbox {
margin-top: 12px;
}
.van-text-dialog__checkbox-label {
margin-left: 8px;
font-size: 15px;
color: #666;
}
.van-text-dialog__button {
margin-top: 24px;
}
</style>
@@ -0,0 +1,46 @@
<template>
<van-dropdown-item :options="yearOptions" @change="yearChange" v-model="year"></van-dropdown-item>
</template>
<script>
module.exports = {
name: "year-van-dropdown-item",
props: {
num: {
type: Number,
default: 10
},
value: {
type: Number,
default: new Date().getFullYear()
}
},
watch: {
value: {
handler: function (val) {
this.year = val
},
immediate: true
}
},
data() {
return {
yearOptions: [],
year: new Date().getFullYear()
}
},
methods: {
yearChange(val) {
this.$emit("input", val)
this.$emit("change", val)
}
},
created() {
for (let i = new Date().getFullYear() - this.num; i <= new Date().getFullYear(); i++) {
this.yearOptions.unshift({ value: i, text: i + "年" })
}
}
}
</script>
<style scoped></style>