ui库和web端产品库合并版本(还需修复细节)

This commit is contained in:
2022-11-29 18:27:14 +08:00
parent 5e4bd93238
commit 8bf6c57668
151 changed files with 28267 additions and 49 deletions

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ yarn-error.log*
/oms/dist/
/project/*/index.js
/project/*/dist
/ui/lib/

View File

@@ -6,6 +6,7 @@ core/
public/
project/
.idea/
ui/lib/
# 忽略指定文件
vue.config.js

1
.npmrc
View File

@@ -1,4 +1,3 @@
registry=http://cli.sinoecare.net/
email=aixianling@sinoecare.com
always-auth=true
_auth="YWRtaW46YWRtaW4xMjM="

5
bin/ui.js Normal file
View File

@@ -0,0 +1,5 @@
const {copyFiles, chalkTag} = require("./tools");
const start = () => {
copyFiles('lib', 'meta').then(() => chalkTag.done("转移成功!"))
}
start()

View File

@@ -1,4 +1,4 @@
import http from "dvcp-ui/lib/js/request";
import http from "dui/lib/js/request";
import Vue from "vue"
export default class PartyOrg {

View File

@@ -4,9 +4,9 @@ import ui from 'element-ui';
import router from './router/router';
import axios from './router/axios';
import utils from './utils';
import vcUI from 'dvcp-ui';
import 'dvcp-ui/lib/styles/common.scss';
import 'dvcp-ui/lib/dvcp-ui.css';
import vcUI from 'dui';
import 'dui/lib/styles/common.scss';
import 'dui/lib/dui.css';
import store from './store';
import dataV from '@jiaminghi/data-view';
import appComps from '../components'
@@ -28,9 +28,9 @@ let theme = null
store.dispatch('getSystem').then(({colorScheme}) => {
theme = JSON.parse(colorScheme || null)
Vue.prototype.$theme = theme?.web || "blue"
return import(`dvcp-ui/lib/styles/theme.${theme?.web}.scss`).catch(() => 0)
return import(`dui/lib/styles/theme.${theme?.web}.scss`).catch(() => 0)
}).finally(() => {
Vue.prototype.$vm = app
!theme ? app.$mount('#app') : import(`dvcp-ui/lib/styles/common.scss`).finally(() => app.$mount('#app'))
!theme ? app.$mount('#app') : import(`dui/lib/styles/common.scss`).finally(() => app.$mount('#app'))
})

View File

@@ -1,4 +1,4 @@
import instance from 'dvcp-ui/lib/js/request'
import instance from 'dui/lib/js/request'
import {Message} from 'element-ui'
let baseURLs = {

View File

@@ -1,7 +1,7 @@
import Vue from 'vue'
import Vuex from 'vuex'
import preState from 'vuex-persistedstate'
import * as modules from "dvcp-ui/lib/js/modules"
import * as modules from "dui/lib/js/modules"
import xsActions from "../../project/xiushan/actions"
Vue.use(Vuex)

View File

@@ -1,6 +1,6 @@
import {MessageBox} from 'element-ui'
import store from '../store'
import tools from 'dvcp-ui/lib/js/utils'
import tools from 'dui/lib/js/utils'
const addChildParty = (parent, pending) => {
let doBeforeCount = pending.length

View File

@@ -6,9 +6,12 @@
"scripts": {
"dev": "vue-cli-service serve",
"lib": "npm unpublish --force&&npm publish",
"ui": "npm i dvcp-ui@latest",
"ui": "npm run build -w ui&&npm i dui@latest",
"sync": "node bin/appsSync.js"
},
"workspaces": [
"ui"
],
"files": [
"packages",
"project",
@@ -21,7 +24,7 @@
"@jiaminghi/data-view": "^2.10.0",
"bin-code-editor": "^0.9.0",
"dayjs": "^1.8.35",
"dvcp-ui": "^1.42.2",
"dui": "file:ui",
"echarts": "^5.1.2",
"hash.js": "^1.1.7",
"mp4box": "^0.4.1",

View File

@@ -62,7 +62,7 @@
<script>
import {mapState} from "vuex";
import AddVaccination from "./addVaccination";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "AppVaccination",

View File

@@ -82,7 +82,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "addVaccination",

View File

@@ -97,7 +97,7 @@
<script>
import {mapState} from 'vuex';
import detail from './detail'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'AppScorePersonal',

View File

@@ -119,7 +119,7 @@
<script>
import { mapState } from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Add',

View File

@@ -163,7 +163,7 @@
<script>
import {mapState} from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Add',

View File

@@ -0,0 +1,26 @@
<template>
<section class="AppModConfig">
</section>
</template>
<script>
export default {
name: "AppModConfig",
label: "应用定制配置",
props: {
instance: Function,
dict: Object,
permissions: Function
},
created() {
this.dict.load('yesOrNo')
}
}
</script>
<style lang="scss" scoped>
.AppModConfig {
height: 100%;
}
</style>

View File

@@ -765,7 +765,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "Add",

View File

@@ -70,7 +70,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "neighbourSetting",

View File

@@ -434,7 +434,7 @@ import {mapState} from "vuex";
import AiEditCard from "./components/AiEditCard";
import PersonalAssets from "./components/personalAssets";
import TagsManage from "./components/tagsManage";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "residentDetail",

View File

@@ -34,7 +34,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "psList",

View File

@@ -328,7 +328,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "hrAdd",

View File

@@ -497,7 +497,7 @@
import {mapState} from "vuex";
import HrMeasure from "./detail/hrMeasure";
import HrLog from "./detail/hrLog";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "hrDetail",

View File

@@ -77,7 +77,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "hrList",

View File

@@ -57,7 +57,7 @@
<script>
import { mapState } from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Add',

View File

@@ -95,7 +95,7 @@
</template>
<script>
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "sealDetail",

View File

@@ -765,7 +765,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "Add",

View File

@@ -70,7 +70,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "neighbourSetting",

View File

@@ -113,7 +113,7 @@
<script>
import { mapState } from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Add',

View File

@@ -52,7 +52,7 @@
</template>
<script>
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Detail',

View File

@@ -46,7 +46,7 @@
<script>
import {mapState} from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'List',

View File

@@ -113,7 +113,7 @@
<script>
import { mapState } from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Add',

View File

@@ -51,7 +51,7 @@
</template>
<script>
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Detail',

View File

@@ -46,7 +46,7 @@
<script>
import {mapState} from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'List',

View File

@@ -113,7 +113,7 @@
<script>
import { mapState } from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Add',

View File

@@ -53,7 +53,7 @@
</template>
<script>
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Detail',

View File

@@ -46,7 +46,7 @@
<script>
import {mapState} from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'List',

View File

@@ -113,7 +113,7 @@
<script>
import { mapState } from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Add',

View File

@@ -52,7 +52,7 @@
</template>
<script>
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Detail',

View File

@@ -46,7 +46,7 @@
<script>
import {mapState} from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils"
import {ID} from "dui/lib/js/utils"
export default {
name: 'List',

View File

@@ -113,7 +113,7 @@
<script>
import { mapState } from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Add',

View File

@@ -55,7 +55,7 @@
</template>
<script>
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'Detail',

View File

@@ -46,7 +46,7 @@
<script>
import {mapState} from 'vuex'
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: 'List',

View File

@@ -1,4 +1,4 @@
import axios from "dvcp-ui/lib/js/request";
import axios from "dui/lib/js/request";
export default {
getFinanceUser({commit}) {

View File

@@ -58,7 +58,7 @@
<script>
import {mapState} from "vuex";
import LoanSta from "./loanSta";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "loanList",

View File

@@ -34,7 +34,7 @@
<script>
import {mapState} from "vuex";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "AppLoanSta",

View File

@@ -95,7 +95,7 @@
</template>
<script>
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "sealDetail",

View File

@@ -57,7 +57,7 @@
<script>
import {mapState} from "vuex";
import DrawInPhone from "./drawInPhone";
import {ID} from "dvcp-ui/lib/js/utils";
import {ID} from "dui/lib/js/utils";
export default {
name: "AppPersonalSignature",

18
ui/meta/cdn/aes.js Normal file

File diff suppressed because one or more lines are too long

5892
ui/meta/cdn/crypto-js.js Normal file

File diff suppressed because it is too large Load Diff

7
ui/meta/cdn/mode-ecb.js Normal file
View File

@@ -0,0 +1,7 @@
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
CryptoJS.mode.ECB = (function () { var a = CryptoJS.lib.BlockCipherMode.extend(); a.Encryptor = a.extend({ processBlock: function (a, b) { this._cipher.encryptBlock(a, b) } }); a.Decryptor = a.extend({ processBlock: function (a, b) { this._cipher.decryptBlock(a, b) } }); return a }())

1
ui/meta/cdn/mpfont.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
/**
* Zero padding strategy.
*/
CryptoJS.pad.ZeroPadding = {
pad: function (data, blockSize) {
// Shortcut
var blockSizeBytes = blockSize * 4
// Pad
data.clamp()
data.sigBytes += blockSizeBytes - ((data.sigBytes % blockSizeBytes) || blockSizeBytes)
},
unpad: function (data) {
// Shortcut
var dataWords = data.words
// Unpad
var i = data.sigBytes - 1
while (!((dataWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff)) {
i--
}
data.sigBytes = i + 1
}
}

1
ui/meta/cdn/weappFont.js Normal file

File diff suppressed because one or more lines are too long

88
ui/meta/js/area.js Normal file
View File

@@ -0,0 +1,88 @@
import request from "./request";
export default class Area {
constructor(code, hash = {}) {
this.id = code
this.level = Area.getLevelByAreaId(code)
this.areaMap = Object.values(this.getAreaInfo(code))
if (Object.keys(hash).length > 0) {
this.getName(this.areaMap.map(id => hash[id]))
}
}
/**
* 获取地区的行政等级
* @param code 地区编码
* @returns {number}
*/
static getLevelByAreaId(code) {
if (code) {
if (code.length === 2 || /0{10}$/.test(code)) return 0;
else if (/0{8}$/.test(code)) return 1;
else if (/0{6}$/.test(code)) return 2;
else if (/0{3}$/.test(code)) return 3;
else return 4
} else return -1
}
/**
* 根据地区编码获取指定等级的地区编码
* @param value 地区编码
* @param level 指定等级
* @returns {string|null|*}
*/
static getAreaCodeByLevel(value, level) {
if (value) {
const areaNumber = value.toString();
switch (level) {
case 0:
return areaNumber.substring(0, 2) + '0000000000';
case 1:
return areaNumber.substring(0, 4) + '00000000';
case 2:
return areaNumber.substring(0, 6) + '000000';
case 3:
return areaNumber.substring(0, 9) + '000';
case 4:
return areaNumber
}
} else return null
}
static createByAction(areaId, ins = request, action = "/admin/area/getAllParentAreaId") {
return ins.post(action, null, {params: {areaId}, withoutToken: 1}).then(res => res?.data?.reverse() || [])
}
getAreaInfo(id) {
let info = {}
const currentLevel = Area.getLevelByAreaId(id);
for (let i = 0; i <= currentLevel; i++) {
info[i] = Area.getAreaCodeByLevel(id, i);
}
return info
}
async getAreaName() {
const names = await Area.createByAction(this.id);
this.getName(names)
}
getName(names) {
this.meta = names
this.nameMap = names?.map(e => e.name) || []
this.name = names?.slice(-1)?.[0]?.name
this.fullname = this.nameMap.join('')
}
/**
* 异步从数据库中获取地区信息
* @returns {Promise<void>}
*/
static async init(code) {
const names = await Area.createByAction(code),
area = new Area(code)
area.getName(names)
return area
}
}

20
ui/meta/js/coin.js Normal file
View File

@@ -0,0 +1,20 @@
export default {
cny(money) {
if (money) {
money.toLocaleString('zh-Hans-CN', {style: 'currency', currency: "CNY"})
}
return money
},
cn(money) {
let num = parseFloat(money), cnMoney = '',
units = '仟佰拾亿仟佰拾万仟佰拾元角分',
cnNum = '零壹贰叁肆伍陆柒捌玖'
num = num.toFixed(2).replace(/\./g,'')
units = units.substring(units.length - num.length)
console.log(num, units.length, num.length)
Array.from(num).map((e, i) => {
cnMoney += cnNum.charAt(e) + units.charAt(i)
})
return cnMoney.replace(/零角零分$/, '整').replace(/零[仟佰拾]/g, '零').replace(/零{2,}/g, '零').replace(/零([亿|万])/g, '$1').replace(/零+元/, '元').replace(/亿零{0,3}万/, '亿').replace(/^元/, "零元")
}
}

77
ui/meta/js/dict.js Normal file
View File

@@ -0,0 +1,77 @@
import request from "./request";
/**
* 封装字典工具类
*/
const $dict = {
url: "/admin/dictionary/queryValsByCodeList",
loading: [],
resolves: [],
getStorage() {
const dicts = JSON.parse(localStorage.getItem('dicts') || null)
return dicts?.data || dicts || [];
},
setUrl(v) {
this.url = v
},
getData(codeList) {
codeList = [codeList].flat().filter(Boolean).toString()
return request.post(this.url, null, {
withoutToken: true, params: {codeList}
}).then(res => res?.data && this.setStorage(res.data))
},
load(...code) {
return new Promise(resolve => {
this.resolves.push(resolve)
if (this.loading.length == 2) {
const [timer, codes] = this.loading;
clearTimeout(timer)
code = Array.from(new Set([codes, code].flat()))
}
const timer = setTimeout(() => {
this.getData(code).then(() => Promise.all(this.resolves.map(e => e())).then(() => this.resolves = []))
}, 500)
this.loading = [timer, code]
})
},
setStorage(data) {
let ds = this.getStorage()
data.map(p => {
if (ds.some(d => d.key == p.key)) {
const index = ds.findIndex(d => d.key == p.key)
ds.splice(index, 1, p)
} else {
ds.push(p)
}
})
localStorage.setItem("dicts", JSON.stringify([ds].flat()))
},
getDict(key) {
let dict = this.getStorage().find(e => e.key == key)
!dict && console.warn("字典%s缺少加载...", key)
return dict ? dict.values : []
},
getValue(key, label) {
let dict = this.getDict(key)
if (dict) {
let item = dict.find(v => v.dictName == label)
return item ? item.dictValue : label
} else return label
},
getLabel(key, value) {
let dict = this.getDict(key)
if (dict) {
let item = dict.find(v => v.dictValue == value)
return item ? item.dictName : value
} else return value
},
getColor(key, value) {
let dict = this.getDict(key)
if (dict) {
let item = dict.find(v => v.dictValue == value)
return item ? item.dictColor : value
} else return value
},
}
export default $dict

23
ui/meta/js/encryption.js Normal file
View File

@@ -0,0 +1,23 @@
import CryptoJs from "../cdn/crypto-js";
/**
* 密码加密工具
* @param params
* @param c 加载尝试计数器
* @returns {string}
*/
export const $encryption = (params, c = 0) => {
if (CryptoJs) {
const key = "thanks,villcloud"
let iv = CryptoJs.enc.Latin1.parse(key)
let encrypted = CryptoJs.AES.encrypt(params.password, iv, {
iv,
mode: CryptoJs.mode.CBC,
padding: CryptoJs.pad.ZeroPadding
})
return encrypted.toString()
} else if (c < 10) {
setTimeout(() => $encryption(params, ++c), 200)
} else console.error("无法加载CryptoJs")
}
export default $encryption

238
ui/meta/js/identity.js Normal file
View File

@@ -0,0 +1,238 @@
import dayjs from "./moment";
const PARAMS = {
/* 省,直辖市代码表 */
provinceAndCitys: {
11: '北京',
12: '天津',
13: '河北',
14: '山西',
15: '内蒙古',
21: '辽宁',
22: '吉林',
23: '黑龙江',
31: '上海',
32: '江苏',
33: '浙江',
34: '安徽',
35: '福建',
36: '江西',
37: '山东',
41: '河南',
42: '湖北',
43: '湖南',
44: '广东',
45: '广西',
46: '海南',
50: '重庆',
51: '四川',
52: '贵州',
53: '云南',
54: '西藏',
61: '陕西',
62: '甘肃',
63: '青海',
64: '宁夏',
65: '新疆',
71: '台湾',
81: '香港',
82: '澳门',
91: '国外'
},
/* 每位加权因子 */
powers: [
'7',
'9',
'10',
'5',
'8',
'4',
'2',
'1',
'6',
'3',
'7',
'9',
'10',
'5',
'8',
'4',
'2'
],
/* 第18位校检码 */
parityBit: ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'],
/* 性别 */
genders: {1: '男', 0: '女'},
}
const Check = {
/* 校验地址码 */
checkAddressCode(addressCode) {
const check = /^[1-9]\d{5}$/.test(addressCode)
if (!check) return false
return !!PARAMS.provinceAndCitys[parseInt(addressCode.substring(0, 2))]
},
/* 校验日期码 */
checkBirthDayCode(birDayCode) {
const check = /^[1-9]\d{3}((0[1-9])|(1[0-2]))((0[1-9])|([1-2][0-9])|(3[0-1]))$/.test(
birDayCode
)
if (!check) return false
const yyyy = parseInt(birDayCode.substring(0, 4), 10)
const mm = parseInt(birDayCode.substring(4, 6), 10)
const dd = parseInt(birDayCode.substring(6), 10)
const xdata = new Date(yyyy, mm - 1, dd)
if (xdata > new Date()) {
return false // 生日不能大于当前日期
} else {
return (
xdata.getFullYear() == yyyy &&
xdata.getMonth() == mm - 1 &&
xdata.getDate() == dd
)
}
},
/* 验证校检码 */
checkParityBit(idCardNo) {
const parityBit = idCardNo.charAt(17).toUpperCase()
return this.getParityBit(idCardNo) == parityBit
},
// 校验15位的身份证号码
check15IdCardNo(idCardNo) {
// 15位身份证号码的基本校验
let check = /^[1-9]\d{7}((0[1-9])|(1[0-2]))((0[1-9])|([1-2][0-9])|(3[0-1]))\d{3}$/.test(
idCardNo
)
if (!check) return false
// 校验地址码
const addressCode = idCardNo.substring(0, 6)
check = this.checkAddressCode(addressCode)
if (!check) return false
const birDayCode = '19' + idCardNo.substring(6, 12)
// 校验日期码
return this.checkBirthDayCode(birDayCode)
},
// 校验18位的身份证号码
check18IdCardNo(idCardNo) {
// 18位身份证号码的基本格式校验
let check = /^[1-9]\d{5}[1-9]\d{3}((0[1-9])|(1[0-2]))((0[1-9])|([1-2][0-9])|(3[0-1]))\d{3}(\d|x|X)$/.test(
idCardNo
)
if (!check) return false
// 校验地址码
const addressCode = idCardNo.substring(0, 6)
check = this.checkAddressCode(addressCode)
if (!check) return false
// 校验日期码
const birDayCode = idCardNo.substring(6, 14)
check = this.checkBirthDayCode(birDayCode)
if (!check) return false
// 验证校检码
return this.checkParityBit(idCardNo)
},
/* 计算校检码 */
getParityBit(idCardNo) {
const id17 = idCardNo.substring(0, 17)
/* 加权 */
let power = 0
for (let i = 0; i < 17; i++) {
power += parseInt(id17.charAt(i), 10) * parseInt(PARAMS.powers[i])
}
/* 取模 */
const mod = power % 11
return PARAMS.parityBit[mod]
}
}
export default class Identity {
constructor(code) {
this.code = this.getId18(code)
this.init()
}
init() {
const {code} = this
if (!!code) {
this.hideCode = Identity.hideId(code)
this.getIdCardInfo(code)
}
}
static hideId(code) {
return code?.replace(/^(\d{10})\d{4}(.{4}$)/g, `$1${Array(5).join('*')}$2`)
}
getId18(code) {
if (code.length == 15) {
const id17 = code.substring(0, 6) + '19' + code.substring(6)
const parityBit = Check.getParityBit(id17)
return id17 + parityBit
} else if (code.length == 18) {
return code
} else {
return null
}
}
getIdCardInfo(idCardNo) {
this.birthday = Identity.getBirthday(idCardNo)
this.sex = Identity.getSex(idCardNo)
this.gender = PARAMS.genders[this.sex]
this.age = Identity.getAge(idCardNo)
this.areaId = Identity.getArea(idCardNo)
}
/**
* 获取性别
* @param code
* @returns {string}
*/
static getSex(code) {
return (Number(code.substring(16, 17)) % 2).toString()
}
/**
* 获取年龄
* @param code
* @returns {number}
*/
static getAge(code) {
const birthday = dayjs(code.substring(6, 14), 'YYYYMMDD')
return Math.ceil(dayjs.duration(dayjs().unix() - dayjs(birthday).unix(), 's').asYears())
}
/**
* 获取地区编码
* @param code
*/
static getArea(code) {
return code.substring(0, 6) + new Array(7).join(0)
}
/**
* 获取生日
*/
static getBirthday(code) {
return dayjs(code.substring(6, 14), 'YYYYMMDD').format("YYYY-MM-DD").replace('Invalid Date', '')
}
/* 校验15位或18位的身份证号码 */
static check(idCardNo) {
// 15位和18位身份证号码的基本校验
const check = /^\d{15}|(\d{17}(\d|x|X))$/.test(idCardNo)
if (!check) return false
// 判断长度为15位或18位
if (idCardNo.length == 15) {
return Check.check15IdCardNo(idCardNo)
} else if (idCardNo.length == 18) {
return Check.check18IdCardNo(idCardNo)
} else {
return false
}
}
}

305
ui/meta/js/modules.js Normal file
View File

@@ -0,0 +1,305 @@
import http from "./request";
import utils from "./utils";
import Vue from "vue"
export const sys = {
state: () => ({
info: {},
theme: {}
}),
mutations: {
setSysInfo(state, info) {
state.info = info
},
setTheme(state, theme) {
state.theme = theme
}
},
actions: {
getSystem({commit}, info) {
return http.post("/app/appdvcpconfig/getSystemInfo", null, {withoutToken: true}).then(res => {
if (res?.data) {
let {systemInfo, colorScheme, enableGreyFilter} = res.data
systemInfo = JSON.parse(systemInfo || null) || {}
colorScheme = JSON.parse(colorScheme || null) || {}
commit("setSysInfo", {...info, ...systemInfo})
commit("setTheme", {colorScheme, enableGreyFilter})
return res.data
} else return Promise.reject()
}).catch(() => commit("setSysInfo", info))
}
}
}
/**
* 用户信息
*/
export const user = {
state: () => ({
token: "",
info: {},
routes: []
}),
mutations: {
setToken(state, token) {
state.token = token;
},
setUserInfo(state, info) {
state.info = info
},
cleanUserInfo(state) {
state.info = {}
state.token = ""
},
setUserExtra(state, extra = {}) {
Object.keys(extra).map(e => Vue.set(state, e, extra[e]))
},
setRoutes(state,routes){
state.routes = routes
}
},
actions: {
getToken({commit}, params) {
let action = "/auth/oauth/token"
if (params?.action) {
action = params?.action
delete params?.action
}
const password = utils.$encryption(params)
return http.post(action, null, {
auth: {
username: 'villcloud',
password: "villcloud"
},
params: {grant_type: 'password', scope: 'server', ...params, password}
}).then(res => {
if (res?.access_token) {
const {token_type, access_token} = res, token = [token_type, access_token].join(" ")
return commit('setToken', token)
}
})
},
getUserInfo({commit, dispatch}) {
return http.post("/admin/user/detail-phone").then(res => {
if (res?.data) {
commit("setUserInfo", res.data)
return Promise.all([dispatch('getWorkflowConfigs')]).then(() => res.data)
}
})
},
getRouteName({state}, appName) {
return state.routes.find(e => e.component == appName)?.route || appName
}
}
}
/**
* 企微jssdk功能
*/
let timer = {injectJWeixin: null, initOpenData: null}
export const wxwork = {
state: () => ({
agentSignURL: "",
apiList: [],
config: {}
}),
mutations: {
setConfig(state, config) {
state.config = config
},
setAgentSignURL(state, url) {
state.agentSignURL = url
},
setApiList(state, list) {
state.apiList = list
},
},
actions: {
agentSign({state, commit, rootState}, params) {
//授权jssdk在url上使用,并获取corpId
let url = window.location.href
if (state.agentSignURL == url && state.config.corpId) {
return Promise.resolve()
} else {
commit("setAgentSignURL", url)
commit("setApiList", [])
let action = "/app/wxcptp/portal/agentSign"
if (!!params?.action) {
action = params.action
delete params.action
}
const {corpId} = rootState.user.info
return http.post(action, null, {
withoutToken: true,
params: {corpId, ...params, url}
}).then(res => {
if (res?.data) {
let config = {
...params,
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来若要查看传入的参数可以在pc端打开参数信息会通过log打出仅在pc端时才会打印。
beta: true,// 必须这么写否则wx.invoke调用形式的jsapi会有问题
corpid: res.data.corpid, // 必填企业微信的corpid必须与当前登录的企业一致
agentid: res.data.agentId, // 必填企业微信的应用id e.g. 1000247
timestamp: Number(res.data.timestamp), // 必填,生成签名的时间戳
nonceStr: res.data.nonceStr, // 必填,生成签名的随机串
signature: res.data.signature,// 必填,签名,见 附录-JS-SDK使用权限签名算法
...res.data
}
commit("setConfig", config)
return config
}
}).catch(err => {
commit("setAgentSignURL", "")
console.error(err)
})
}
},
injectJWeixin({state, commit}, apis) {
const inject = (jsApiList) => new Promise((resolve, reject) => {
jsApiList = jsApiList || []
if (timer.injectJWeixin) {//节流设置,50ms内的多次请求合并到一处
clearTimeout(timer.injectJWeixin)
jsApiList = [...new Set([...state.apiList, ...jsApiList])]
commit("setApiList", jsApiList)
}
timer.injectJWeixin = setTimeout(() => {
const sdk = wx?.agentConfig ? wx : jWeixin
sdk?.agentConfig({
...state.config, jsApiList,
success: res => resolve(res),
fail: err => {
console.error(err)
reject(err)
}
})
}, 50)
})
return inject(apis)
},
initOpenData({dispatch, commit}, params = {}) {
const initWOD = (count = 0) => {
if (!!window?.WWOpenData) {
const canvas = params?.canvas
if (canvas) delete params.canvas
if (timer.initOpenData) {
clearTimeout(timer.initOpenData)
}
const init = () => canvas ? dispatch('initCanvas') : dispatch('bindElements')
timer.initOpenData = setTimeout(() => {
window?.WWOpenData?.checkSession({
success: () => init(),
fail: () => {
dispatch('agentSign', params).then(() => dispatch("injectJWeixin")).then(() => init())
}
})
}, 50)
} else if (count > 10) {
console.log("无法获取WWOpenData")
} else {
setTimeout(() => {
initWOD(++count)
}, 200)
}
}
dispatch('agentSign', params).then(() => dispatch("injectJWeixin")).then(() => initWOD())
},
bindElements() {
const nodes = document.querySelectorAll('.AiOpenData')
window.WWOpenData?.bindAll(nodes)
},
initCanvas() {
window.WWOpenData?.initCanvas()
},
transCanvas(store, items) {
return new Promise((resolve, reject) => {
window.WWOpenData?.prefetch({items}, (err, data) => {
err ? reject(err) : resolve(data)
})
})
}
}
}
/**
* 各种前端方案记录选择
*/
export const logs = {
state: () => ({
closeIntro: []
}),
mutations: {
addCloseIntro(state, app) {
state.closeIntro.push(app)
}
},
}
/**
* 流程信息
*/
const startProcess = (form) => {
const {bid, app, flows = {}} = form
const process = flows[app]
if (!!process) {
const {id: pid, config} = process
let workflowConfig = JSON.parse(config || null), nowNodeId = [], startId
workflowConfig?.nodes?.map(e => {
if (e.type == "start") {
e.properties.isStart = true
e.properties.updateTime = utils.$moment().format("YYYY-MM-DD HH:mm:ss")
startId = e.id
}
})
workflowConfig?.edges?.map(e => {
if (e.sourceNodeId == startId) {
nowNodeId.push(e.targetNodeId)
}
})
nowNodeId = nowNodeId?.toString()
return !!nowNodeId && http.post("/app/appworkflowlog/addOrUpdate", {bid, pid, nowNodeId, workflowConfig: JSON.stringify(workflowConfig)})
}
}
export const workflow = {
state: () => ({}),
mutations: {
setWfConfigs(state, configs) {
configs.map(e => {
Vue.set(state, e.app, e)
})
}
},
actions: {
getWorkflowConfigs({commit}) {
return http.post("/app/appworkflowmanage/list", null, {
params: {size: 999}
}).then(res => {
if (res?.data) {
return commit("setWfConfigs", res.data.records)
}
}).catch(() => 0)
},
startFlow(context, form) {
startProcess(form)
},
endFlow(context, form) {
let {workflowConfig = {}, nowNodeId} = form
workflowConfig?.nodes?.map(e => {
if (e.type == "end") {
e.properties.isFinished = true
e.properties.updateTime = utils.$moment().format("YYYY-MM-DD HH:mm:ss")
nowNodeId = "nowNodeId"
}
})
return http.post("/app/appworkflowlog/addOrUpdate", {...form, nowNodeId, workflowConfig: JSON.stringify(workflowConfig)})
}
},
processAdapter(config) {//流程业务拦截器
if (/addOrUpdate/.test(config.url)) {
let app = config.url.replace(/.+(app[^\\\/]+).+/g, '$1')
if (/appapplicationinfo/.test(app)) {//动态台账表单
app = config.params?.appId
} else if (/appcontentinfo/.test(app)) {//内容发布
app = config.data?.moduleId
}
config.workflow = app
}
return config
},
startProcess
}

35
ui/meta/js/moment.js Normal file
View File

@@ -0,0 +1,35 @@
import moment from 'dayjs'
import duration from 'dayjs/plugin/duration'
import updateLocale from 'dayjs/plugin/updateLocale'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import 'dayjs/locale/zh-cn'
moment.locale('zh-cn')
moment.extend(updateLocale)
moment.extend(customParseFormat)
moment.extend(duration)
moment.updateLocale('zh-cn', {
weekdays: "星期日|星期一|星期二|星期三|星期四|星期五|星期六".split("|"),
meridiem(hour) {
let word = ""
if (hour < 6) {
word = "凌晨"
} else if (hour < 9) {
word = "早上"
} else if (hour < 12) {
word = "上午"
} else if (hour < 14) {
word = "中午"
} else if (hour < 17) {
word = "下午"
} else if (hour < 19) {
word = "傍晚"
} else if (hour < 22) {
word = "晚上"
} else {
word = "夜里"
}
return word;
}
})
export default moment

83
ui/meta/js/request.js Normal file
View File

@@ -0,0 +1,83 @@
/* eslint-disable */
import axios from 'axios'
import {Message} from "element-ui";
import {workflow} from "./modules";
const instance = axios.create({
baseURL: process.env.NODE_ENV === "production" ? "/" : "/lan",
timeout: 600000,
withCredentials: true,
})
const getStore = () => JSON.parse(localStorage.getItem("vuex") || null) || {}
const getToken = () => getStore().user?.token
/**
* 节流工具
*/
let throttleMap = {}
const source = axios.CancelToken.source();
instance.interceptors.request.use(config => {
if (config.throttle) {// 节流处理
let timer = throttleMap[config.url]
if (!!timer) {
config.cancelToken = source.token
source.cancel("节流控制,取消请求:" + config.url)
}
throttleMap[config.url] = setTimeout(() => {
throttleMap[config.url] = null
}, config.throttle)
}
if (!config.withoutToken && getToken()) {
config.headers["Authorization"] = getToken()
}
//BUG 9456 去除传参空格
if (config.params) {
Object.keys(config.params).map(e => {
if (typeof config.params[e] == "string") config.params[e] = config.params[e].trim()
})
}
config = workflow.processAdapter(config)
return config
}, err => {
console.error(err)
})
instance.interceptors.response.use(res => {
if (res && !!res.config.workflow) {
const {config: {workflow: app}, data: {data: bid}} = res
bid && workflow.startProcess({app, bid, flows: getStore().workflow})
}
if (res && res.data) {
if (!!res.data.code?.toString()) {
if (res.data.code == 0) {
return res.data
} else if (!!res.config.pureBack) {
return res.data
} else if (res.data.code == 401) {
console.error("安全令牌验证无法通过!")
return Promise.reject(res.data)
} else {
Message.error(res.data.msg || "请求失败!")
return !!res.config.returnError ? res.data : Promise.reject(res.data.msg)
}
} else return res.data
} else {
Message.error("服务器异常,请联系管理员!")
}
}, err => {
if (err) {
if (err.response) {
if (err.response.status == 401) {
console.error("安全令牌验证无法通过!")
} else {
console.error(err.response.statusText)
}
} else {
console.error(err)
}
} else {
console.error("通信异常,请联系管理员!")
}
})
export default instance

211
ui/meta/js/utils.js Normal file
View File

@@ -0,0 +1,211 @@
import {MessageBox} from 'element-ui'
import $moment from './moment'
import $dict from './dict'
import $encryption from './encryption'
import $coin from './coin'
import Area from "./area"
import ID from "./identity"
/**
* 生成子节点的递归方法
* @param parent 父元素
* @param pending 待递归的数组
* @param config 配置
*/
const addChild = (parent, pending, config) => {
let conf = {
key: 'id',
parent: 'parentId',
children: 'children',
...config
},
doBeforeCount = pending.length
for (let i = pending.length - 1; i >= 0; i--) {
let e = pending[i]
if (e[conf.parent] == parent[conf.key]) {
parent[conf.children] = [...(parent[conf.children] || []), e]
pending.splice(i, 1)
}
}
parent[conf.children] &&
(parent[conf.children] = parent[conf.children].reverse())
if (pending.length > 0 && doBeforeCount > pending.length) {
parent[conf.children].map(c => addChild(c, pending, conf))
}
}
/**
* 数组转tree
* @param list 待转化的数组
* @param config 配置
*/
const $arr2tree = (list, config = {}) => {
const {key = 'id', parent = 'parentId', children = 'children'} = config, result = [], itemMap = {}, ids = list?.map(e => `${e[key]}`)?.toString()
for (const e of list) {
const id = e[key], pid = e[parent]
itemMap[id] = {...e, [children]: [itemMap[id]?.[children]].flat().filter(Boolean)}
const treeItem = itemMap[id]
if (!!pid && ids.indexOf(pid) > -1) {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
} else result.push(treeItem)
}
return result
}
/**
* 封装提示框
*/
const $confirm = (content, options) => {
return MessageBox.confirm(content, {
type: 'warning',
confirmButtonText: '确认',
center: true,
title: '提示',
dangerouslyUseHTMLString: true,
customClass: "AiConfirm",
...options
}).catch(() => 0)
}
const $decimalCalc = (...arr) => {
// 确认提升精度
let decimalLengthes = arr.map(e => {
let index = ('' + e).indexOf('.')
return ('' + e).length - index
})
let maxDecimal = Math.max(...decimalLengthes),
precision = Math.pow(10, maxDecimal)
// 计算
let intArr = arr.map(e => (Number(e) || 0) * precision)
// 返回计算值
return intArr.reduce((t, a) => t + a) / precision
}
/**
* 封装权限判断方法
*/
const $permissions = flag => {
let buttons = []
if (localStorage.getItem('vuex')) {
const vuex = JSON.parse(localStorage.getItem('vuex'))
buttons = vuex.user.info.buttons
}
if (buttons && buttons.length > 0) {
return buttons.some(b => b.id == flag || b.permission == flag)
} else return false
}
const $colorUtils = {
Hex2RGBA(color, alpha = 1) {
let hex = 0
if (color.charAt(0) == '#') {
if (color.length == 4) {
// 检测诸如#FFF简写格式
color =
'#' +
color.charAt(1).repeat(2) +
color.charAt(2).repeat(2) +
color.charAt(3).repeat(2)
}
hex = parseInt(color.slice(1), 16)
}
let r = (hex >> 16) & 0xff
let g = (hex >> 8) & 0xff
let b = hex & 0xff
return `rgba(${r},${g},${b},${alpha})`
},
RGBtoHex(r, g, b) {
let hex = (r << 16) | (g << 8) | b
return '#' + hex.toString(16)
}
}
export const $copy = any => {
if (any) {
return JSON.parse(JSON.stringify(any))
} else return any
}
let debounceWait = null
export const $debounce = (fn, wait) => {
if (debounceWait !== null) clearTimeout(debounceWait);
debounceWait = setTimeout(function () {
typeof fn === 'function' && fn();
}, wait);
}
export const $checkJson = str => {
if (typeof str == 'string') {
try {
let obj = JSON.parse(str);
return !!(typeof obj == 'object' && obj);
} catch (e) {
return false;
}
}
return false;
}
const $load = (sdk, interval = 200, name = "", c = 0) => {
if (!!sdk) {
return Promise.resolve()
} else if (c < 10) {
return new Promise(resolve => setTimeout(() => resolve($load(sdk, interval, name, ++c)), interval))
} else return Promise.reject("无法加载" + name)
}
export {Area, ID}
export default {
addChild,
$confirm,
$decimalCalc,
$dict,
$permissions,
$colorUtils,
$moment,
$encryption,
$coin,
$injectLib: (url, cb = () => 0) => {
const scriptList = document.body.querySelectorAll('script')
if (Object.values(scriptList || {}).findIndex(e => e.src == url) == -1) {
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
script.addEventListener('load', () => cb())
document.body.appendChild(script)
} else cb()
},
$injectCss: (url, cb = () => 0) => {
const linkList = document.body.querySelectorAll('link')
if (Object.values(linkList || {}).findIndex(e => e.href == url) == -1) {
const link = document.createElement('link')
link.rel = "stylesheet"
link.type = "text/css"
link.href = url
link.addEventListener('load', () => cb())
document.head.appendChild(link)
} else cb()
},
$dateFormat: (time, format) => {
return $moment(time)
.format(format || 'YYYY-MM-DD')
.replace('Invalid Date', '')
},
$copy,
$download: url => {
fetch(url).then(res => res.blob()).then(blob => {
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
},
$debounce,
$checkJson,
$arr2tree,
$load
}

View File

@@ -0,0 +1,522 @@
/*
* CKEditor 5 (v34.2.0) content styles.
* Generated on Fri, 29 Jul 2022 13:03:52 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
*/
:root {
--ck-color-base-active: hsl(208, 88%, 52%);
--ck-color-image-caption-background: hsl(0, 0%, 97%);
--ck-color-image-caption-text: hsl(0, 0%, 20%);
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%);
--ck-color-table-caption-background: hsl(0, 0%, 97%);
--ck-color-table-caption-text: hsl(0, 0%, 20%);
--ck-color-table-column-resizer-hover: var(--ck-color-base-active);
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
--ck-highlight-pen-green: hsl(112, 100%, 27%);
--ck-highlight-pen-red: hsl(0, 85%, 49%);
--ck-image-style-spacing: 1.5em;
--ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2);
--ck-table-column-resizer-position-offset: calc(var(--ck-table-column-resizer-width) * -0.5 - 0.5px);
--ck-table-column-resizer-width: 7px;
--ck-todo-list-checkmark-size: 16px;
--ck-z-default: 1;
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 0.9em auto;
min-width: 50px;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline picture,
.ck-content .image-inline img {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left,
.ck-content .image-style-block-align-right {
max-width: calc(100% - var(--ck-image-style-spacing));
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left,
.ck-content .image-style-align-right {
clear: none;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-right {
margin-right: 0;
margin-left: auto;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left {
margin-left: 0;
margin-right: auto;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content p + .image-style-align-left,
.ck-content p + .image-style-align-right,
.ck-content p + .image-style-side {
margin-top: 0;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left,
.ck-content .image-inline.image-style-align-right {
margin-top: var(--ck-inline-image-style-spacing);
margin-bottom: var(--ck-inline-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left {
margin-right: var(--ck-inline-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing);
}
/* ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* ckeditor5-language/theme/language.css */
.ck-content span[lang] {
font-style: italic;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li {
margin-bottom: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li .todo-list {
margin-top: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc(var(--ck-todo-list-checkmark-size) / 3);
top: calc(var(--ck-todo-list-checkmark-size) / 5.3);
width: calc(var(--ck-todo-list-checkmark-size) / 5.3);
height: calc(var(--ck-todo-list-checkmark-size) / 2.6);
border-style: solid;
border-color: transparent;
border-width: 0 calc(var(--ck-todo-list-checkmark-size) / 8) calc(var(--ck-todo-list-checkmark-size) / 8) 0;
transform: rotate(45deg);
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center;
color: var(--ck-color-table-caption-text);
background-color: var(--ck-color-table-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 0.9em auto;
display: table;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table table {
overflow: hidden;
table-layout: fixed;
}
/* ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table td,
.ck-content .table th {
position: relative;
}
/* ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table .table-column-resizer {
position: absolute;
top: -999999px;
bottom: -999999px;
right: var(--ck-table-column-resizer-position-offset);
width: var(--ck-table-column-resizer-width);
cursor: col-resize;
user-select: none;
z-index: var(--ck-z-default);
}
/* ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table[draggable] .table-column-resizer {
display: none;
}
/* ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table .table-column-resizer:hover,
.ck-content .table .table-column-resizer__active {
background-color: var(--ck-color-table-column-resizer-hover);
opacity: 0.25;
}
/* ckeditor5-table/theme/tablecolumnresize.css */
.ck-content[dir=rtl] .table .table-column-resizer {
left: var(--ck-table-column-resizer-position-offset);
right: unset;
}
/* ckeditor5-table/theme/tablecolumnresize.css */
.ck-content.ck-read-only .table .table-column-resizer {
display: none;
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {
background: unset;
padding: 0;
border-radius: 0;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
@media print {
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
padding: 0;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
display: none;
}
}

673
ui/meta/styles/common.scss Normal file
View File

@@ -0,0 +1,673 @@
@import "iconfont/iconfont";
@import 'iconfont/logofont';
@import "ckeditor";
@import "vars";
$cdn: "https://cdn.cunwuyun.cn/";
$--color-primary: $primaryColor;
$--color-text-placeholder: $placeholderColor;
$--border-color-base: $borderColor;
$--color-success: $successColor;
$--color-warning: $warnColor;
$--color-danger: $errorColor;
$--color-info: $infoColor;
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";
/**
常用内外边距样式
*/
@each $padMar, $pm in (mar:margin, pad:padding) {
@each $pos, $p in (l:left, r:right, t:top, b:bottom) {
@each $v in (8, 10, 16, 20, 32, 48, 60) {
.#{$padMar}-#{$pos+$v} {
#{$pm}-#{$p}: #{$v}px
}
}
}
}
/**
不换行文本
*/
.nowarp-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/**
表头式样
*/
.table-header {
background-color: rgba(243, 246, 249, 1) !important;
color: #333;
line-height: 44px;
font-size: 14px;
font-weight: bold;
padding: 0 !important;
border-bottom: none !important;
}
* {
box-sizing: border-box;
}
/**
表行样式
*/
.table-row {
height: 44px;
&:hover, &:hover td.table-cell {
background-color: #EFF6FF !important;
}
}
/**
表格样式
*/
.table-cell {
font-size: 14px;
padding: 0 !important;
.cell {
line-height: 15px;
}
}
/**
图标统一样式
*/
.iconfont {
font-size: 14px;
}
.iconShow:hover, .iconEdit:hover, .iconParent:hover, .iconChange:hover {
color: $primaryColor;
}
.iconDelete:hover {
color: $errorColor;
}
.iconBack_Large {
width: 16px;
height: 16px;
color: $primaryColor;
cursor: pointer;
}
/**
缺省页相关样式
*/
.no-data {
background: url("https://cdn.cunwuyun.cn/ui/svg/NoData.svg") no-repeat center;
background-size: 120px 120px;
height: 120px;
margin: 48px auto 10px;
}
/**
缺省页相关样式
*/
.ai-empty__bg {
background: url("https://cdn.cunwuyun.cn/ui/svg/empty.svg") no-repeat center;
background-size: 120px 120px;
height: 120px;
margin: 48px auto 0;
}
.no-permission {
background: url("https://cdn.cunwuyun.cn/ui/svg/NoAuthority.svg") no-repeat center;
background-size: 120px 120px;
height: 120px;
margin-top: 48px;
}
.no-message {
background: url("https://cdn.cunwuyun.cn/ui/svg/NoMessage.svg") no-repeat center;
background-size: 120px 120px;
height: 120px;
margin-top: 48px;
}
.done-success {
background: url("https://cdn.cunwuyun.cn/ui/svg/Success.svg") no-repeat center;
background-size: 120px 120px;
height: 120px;
}
.no-div-text {
text-align: center;
font-size: 14px;
color: #666
}
.developing {
background: url("https://cdn.cunwuyun.cn/ui/svg/developing.svg") no-repeat center;
background-size: 400px 320px;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
&:after {
margin-top: 270px;
font-size: 14px;
color: $primaryColor;
display: block;
content: "功能正在开发中..."
}
}
/**
placeholder 样式
*/
input::-webkit-input-placeholder {
color: #ccc;
}
input::-moz-placeholder {
color: #ccc;
}
input::-ms-input-placeholder {
color: #ccc;
}
//切换地区Tab 样式
.area-popover {
right: 0;
}
//弹窗树菜单的样式
.dialog-tree {
&.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
background: rgba(239, 246, 255, 1);
border-radius: 2px;
font-weight: normal;
color: rgba(80, 136, 255, 1);
}
}
//发起群聊的按钮样式
.openIM {
width: 26px;
height: 20px;
background: rgba(255, 255, 255, 1);
border-radius: 12px;
border: 1px solid rgba(208, 212, 220, 1);
box-sizing: border-box;
text-align: center;
cursor: pointer;
&.iconGroup_IM {
color: #89B;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
&:hover {
background: $primaryColor;
border-color: $primaryColor;
&.iconGroup_IM {
color: #fff;
}
}
}
.fuzzy {
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
color: transparent !important;
}
/**
马赛克样式
*/
.mosaic {
position: relative;
&:before {
content: "";
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
background-position: 0 0, 10px 10px;
background-size: 20px 20px;
background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee 100%), linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%);
z-index: 202011051009;
}
}
/**
自定义弹性盒子快速用
*/
div[flex] {
display: flex;
align-items: center;
&.column {
flex-direction: column;
}
&.wrap {
flex-wrap: wrap;
}
&.gap {
gap: 20px;
}
}
.fill {
flex: 1;
min-height: 0;
min-width: 0;
}
.el-input {
input[type="number"] {
-moz-appearance: textfield;
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button {
-webkit-appearance: none;
}
}
}
// 2.0公共样式
.ai-form {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
width: 100%;
.el-form-item {
width: 50%;
margin-bottom: 24px;
.el-form-item__content {
margin-left: 134px !important;
}
::v-deep.el-form-item__content {
& .el-input--small, & > .el-range-editor--small, & > .el-cascader, & > .el-select {
width: 325px;
}
}
}
}
.el-message-box {
width: 520px;
padding-bottom: 0;
.el-message-box__header {
padding-left: 40px;
}
.el-message-box__content {
padding-left: 77px;
padding-top: 4px;
}
.el-message-box__title {
justify-content: start;
font-size: 14px;
.el-icon-error:before {
content: "\e7a3";
}
}
.el-message-box__status {
padding-right: 12px;
}
.el-message-box__message {
text-align: start;
font-size: 16px;
color: #333;
font-weight: bold;
min-height: 60px;
}
.el-message-box__btns {
box-sizing: border-box;
text-align: center;
background-color: #F3F6F9;
display: flex;
align-items: center;
justify-content: center;
height: 64px;
.el-button {
padding: 0 32px;
}
}
}
.table-options {
.el-button--text {
height: auto;
margin-right: 8px !important;
margin-left: 0 !important;
padding: 0;
border-radius: unset;
&:last-child {
margin-right: 0 !important;
}
}
span {
margin-right: 8px;
color: $primaryColor;
font-size: 14px;
&:last-child {
margin-right: 0;
}
&:hover {
opacity: 0.8;
}
}
.is-disabled span {
color: #999 !important;
}
}
textarea {
font-family: inherit;
}
.el-button--primary {
border: none;
background: $primaryBtnColor;
&:hover {
opacity: 0.6;
background: $primaryBtnColor;
}
}
.el-button {
font-size: 14px;
border-radius: 2px;
height: 32px;
padding: 8px;
box-sizing: border-box;
[class*=iconfont] {
font-size: 14px;
& + span {
margin-left: 5px;
}
}
}
.el-button--text [class*=iconfont] {
color: inherit;
font-size: inherit;
}
.wechat-message__container {
padding: 8px !important;
background: #F6F9FF !important;
border-radius: 2px !important;
color: #222222 !important;
font-size: 14px !important;
border: 1px solid $primaryColor !important;
h2 {
color: #222222;
font-weight: 500;
font-size: 14px;
}
}
h1, h2, h3, p {
margin: 0;
}
.el-tooltip__popper {
max-width: 600px;
word-break: break-all;
}
.ai-personselect .el-input__suffix .el-input__validateIcon {
display: none;
}
.AiWechatSelecter-container .el-input__suffix .el-input__validateIcon {
display: none;
}
.AiWechatSelecter-container .el-input__suffix .el-input__validateIcon {
display: none;
}
.icon-color89B {
color: #89B;
}
// flex 布局
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-start {
display: flex;
align-items: center;
}
.ailist-wrapper {
div, p, h2, h1, h3, h5, span {
box-sizing: border-box;
}
}
.mt10 {
margin-top: 10px;
}
.ai-scan {
display: flex;
position: absolute;
top: -10px;
right: -14px;
cursor: pointer;
.poptip-arrow {
position: relative;
height: 24px;
line-height: 24px;
margin-right: 8px;
margin-top: 4px;
padding: 0 10px;
color: $primaryColor;
font-size: 12px;
text-align: center;
background: $primaryLightColor;
border: 1px solid $primaryColor;
em {
position: absolute;
width: 0;
height: 0;
border-color: hsla(0, 0%, 100%, 0);
border-style: solid;
overflow: hidden;
top: 50%;
right: -6px;
transform: translateY(-50%);
border-left-color: $primaryLightColor;
border-width: 6px 0 6px 6px;
}
a {
position: absolute;
width: 0;
height: 0;
border-color: hsla(0, 0%, 100%, 0);
border-style: solid;
overflow: hidden;
top: 50%;
right: -7px;
transform: translateY(-50%);
border-left-color: $primaryColor;
border-width: 6px 0 6px 6px;
}
}
& > i {
padding: 0;
color: $primaryColor;
font-size: 48px;
}
}
/**
背景图设置
*/
.signLeftBg {
background-image: url('#{$cdn}/ui/background/#{$theme}/loginLeft.png');
}
.navBg {
background-image: url('#{$cdn}/ui/background/#{$theme}/nav_bg.png');
}
/**
特殊样式字体
*/
.projectName {
font-family: "Microsoft YaHei UI", "Microsoft YaHei", serif;
height: 56px;
line-height: 56px;
font-size: 42px;
font-weight: 800;
white-space: nowrap;
position: relative;
text-shadow: 0 6px 3px rgba(#222, .1);
color: transparent;
&:after {
position: absolute;
left: 0;
content: attr(title);
background: $projectName;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
z-index: 99;
}
&:before {
position: absolute;
left: 0;
content: attr(title);
text-shadow: 3px 0 0 #fff, -3px 0 0 #fff, 0 -3px 0 #fff, 0 3px 0 #fff;
z-index: 66;
}
}
.textShadow {
position: absolute;
top: 0;
text-shadow: 0 2px 0 $textShadow;
color: transparent;
z-index: -1;
}
#ai-waiting {
color: $primaryColor
}
.color-primary {
color: $primaryColor
}
.hoverActive {
&:hover, &.current, &.isActive {
color: $primaryColor !important;
}
}
/** 登录页左侧标题样式 **/
.signLeftContent {
.titlePane {
margin-top: 124px;
font-size: 20px;
margin-bottom: 64px;
& > b {
display: block;
font-size: 40px;
margin-bottom: 16px;
}
}
.subTitle {
margin-bottom: 16px;
opacity: 0.8;
display: flex;
align-items: center;
gap: 6px;
&:before {
content: " ";
background: transparent;
border-radius: 50%;
border: 1px solid #fff;
width: 8px;
height: 8px;
}
&:last-of-type {
margin-bottom: 0;
}
}
}
.AiConfirm {
position: fixed;
z-index: 202210261023;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/**
节流器
*/
.throttle {
animation: throttle 0.5s step-end forwards;
}
.throttle:active {
animation: none;
}
@keyframes throttle {
from {
pointer-events: none;
}
to {
pointer-events: all;
}
}

View File

@@ -0,0 +1,958 @@
@font-face {
font-family: "iconfont"; /* Project id 1309749 */
src: url('//at.alicdn.com/t/c/font_1309749_t7bf1dhlp7e.woff2?t=1666749875458') format('woff2'),
url('//at.alicdn.com/t/c/font_1309749_t7bf1dhlp7e.woff?t=1666749875458') format('woff'),
url('//at.alicdn.com/t/c/font_1309749_t7bf1dhlp7e.ttf?t=1666749875458') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.iconpingfenkaohe:before {
content: "\e736";
}
.iconcloss:before {
content: "\e735";
}
.iconzhengcefuwu:before {
content: "\e731";
}
.iconpeixunguanli:before {
content: "\e730";
}
.iconxiaoxizhongxin:before {
content: "\e72f";
}
.iconzhaopinhuiguanli:before {
content: "\e72e";
}
.iconshuzixiangcun:before {
content: "\e72d";
}
.iconteshurenqun:before {
content: "\e72c";
}
.iconzhengminhudong:before {
content: "\e72b";
}
.iconjicengzhili:before {
content: "\e72a";
}
.iconxiaochengxuguanli:before {
content: "\e729";
}
.iconyingjizhihui:before {
content: "\e728";
}
.iconjicengdangjian:before {
content: "\e727";
}
.iconxiangcunchanye:before {
content: "\e726";
}
.iconfangfanpin:before {
content: "\e725";
}
.iconnumber:before {
content: "\e724";
}
.iconattachments:before {
content: "\e719";
}
.icondate:before {
content: "\e71a";
}
.iconcard:before {
content: "\e71b";
}
.iconid:before {
content: "\e71c";
}
.iconcascade:before {
content: "\e71d";
}
.iconlocation:before {
content: "\e71e";
}
.iconname:before {
content: "\e71f";
}
.iconrich_text:before {
content: "\e720";
}
.iconphone_number:before {
content: "\e721";
}
.iconswitch:before {
content: "\e722";
}
.icontime:before {
content: "\e723";
}
.icondianliang:before {
content: "\e711";
}
.iconxueya:before {
content: "\e714";
}
.iconzhuangtai:before {
content: "\e715";
}
.iconxinlv:before {
content: "\e716";
}
.icontiwen:before {
content: "\e717";
}
.iconxueyang:before {
content: "\e718";
}
.iconzongzhizuzhi:before {
content: "\e710";
}
.iconvideolink:before {
content: "\e70f";
}
.iconfacial_recognition:before {
content: "\e70e";
}
.iconunpublish:before {
content: "\e70c";
}
.iconpreview:before {
content: "\e708";
}
.iconpublish:before {
content: "\e709";
}
.iconmore:before {
content: "\e70a";
}
.iconshare:before {
content: "\e70b";
}
.iconcheck_box:before {
content: "\e700";
}
.iconline:before {
content: "\e701";
}
.iconradio:before {
content: "\e702";
}
.icontext_box:before {
content: "\e703";
}
.icontext_area:before {
content: "\e704";
}
.iconpage:before {
content: "\e705";
}
.iconpic:before {
content: "\e706";
}
.iconSelect:before {
content: "\e707";
}
.iconhuluhuxian:before {
content: "\e6f6";
}
.iconkaoheguanli:before {
content: "\e6f9";
}
.iconhujiaozhongxin:before {
content: "\e6fa";
}
.iconliangxinzuzhi:before {
content: "\e6fb";
}
.iconxiaoyuananfang:before {
content: "\e6fc";
}
.iconzhongdianqingshaonian:before {
content: "\e6fd";
}
.iconanquanshengchan:before {
content: "\e6fe";
}
.iconshehuizhian:before {
content: "\e6ff";
}
.iconguangbofabu:before {
content: "\e6f4";
}
.iconwangshangbanshi:before {
content: "\e6f3";
}
.iconyunyingzhongxin:before {
content: "\e6f1";
}
.iconzhengwuweixin:before {
content: "\e6f2";
}
.iconkaoqinguanli:before {
content: "\e6eb";
}
.iconshijianguanli:before {
content: "\e6ec";
}
.iconyifangzhaoren:before {
content: "\e6ed";
}
.iconwanggeguanli:before {
content: "\e6ee";
}
.iconxinfangguanli:before {
content: "\e6ef";
}
.iconshipinjiankong:before {
content: "\e6f0";
}
.iconloudongmoxing:before {
content: "\e6ea";
}
.iconwanggeyuan:before {
content: "\e6e8";
}
.iconjinqishijian:before {
content: "\e6e6";
}
.iconxiaoquzonglan:before {
content: "\e6e7";
}
.iconloudongxinxi:before {
content: "\e6e9";
}
.iconchuangyebutie:before {
content: "\e6db";
}
.icondanweiguanli:before {
content: "\e6dc";
}
.iconjiuyefuwu:before {
content: "\e6e2";
}
.iconchuangyejiuyeguanli:before {
content: "\e6e3";
}
.iconchuangyedanbaodaikuan:before {
content: "\e6e4";
}
.icondaxueshengshixishixun:before {
content: "\e6e5";
}
.iconsearch:before {
content: "\e732";
}
.iconxqhd:before {
content: "\e733";
}
.iconwdhd:before {
content: "\e734";
}
.iconrobot:before {
content: "\e6da";
}
.iconfangda:before {
content: "\e6d7";
}
.iconsuoxiao:before {
content: "\e6d8";
}
.iconEarth:before {
content: "\e6d9";
}
.iconxianfengyeweihui:before {
content: "\e6d6";
}
.iconhuiyuanguanli:before {
content: "\e6d1";
}
.iconcunganbuguanli:before {
content: "\e6d2";
}
.iconzhaopinguanli:before {
content: "\e6d3";
}
.iconqiyeguanli:before {
content: "\e6d5";
}
.iconxingfujifen:before {
content: "\e6ce";
}
.iconxinxizhongxin:before {
content: "\e6cf";
}
.iconpinyipin:before {
content: "\e6d0";
}
.iconUnpublish:before {
content: "\e6cc";
}
.iconPublish:before {
content: "\e6cd";
}
.iconDelay:before {
content: "\e6cb";
}
.iconwarning:before {
content: "\e6f5";
}
.iconzpg:before {
content: "\e6f7";
}
.iconsqy:before {
content: "\e6f8";
}
.iconzxjyzdls:before {
content: "\e6dd";
}
.iconzxjygwgl:before {
content: "\e6de";
}
.iconzxjywdzy:before {
content: "\e6df";
}
.iconzxjycydb:before {
content: "\e6e0";
}
.iconzxjyckmb:before {
content: "\e6e1";
}
.iconarea:before {
content: "\e6d4";
}
.iconyiqingfangkong:before {
content: "\e6ca";
}
.iconPostil:before {
content: "\e6c9";
}
.iconjiaonadangfei:before {
content: "\e6c8";
}
.iconSuccess:before {
content: "\e6c7";
}
.iconAccount_Login:before {
content: "\e6c5";
}
.iconQR_code:before {
content: "\e6c6";
}
.iconMediaPlayer_Play:before {
content: "\e6c3";
}
.iconMediaPlayer_Stop:before {
content: "\e6c4";
}
.icondangyuan:before {
content: "\e6b0";
}
.iconEnvironment:before {
content: "\e6bc";
}
.iconLaw:before {
content: "\e6bd";
}
.iconjicengbangong:before {
content: "\e6be";
}
.iconjicengzhuzhi:before {
content: "\e6bf";
}
.iconwenmingxiangfeng:before {
content: "\e6c0";
}
.iconyangguangcunwu:before {
content: "\e6c1";
}
.iconminzhuyishi:before {
content: "\e6c2";
}
.iconshehuijiuzhu:before {
content: "\e6b8";
}
.iconshujugongxiang:before {
content: "\e6b9";
}
.iconjuminxinxi:before {
content: "\e6ba";
}
.iconxinxiguanli:before {
content: "\e6bb";
}
.iconRecommend:before {
content: "\e6af";
}
.iconqiandao:before {
content: "\e6ad";
}
.iconqingjia:before {
content: "\e6ae";
}
.iconhistogram:before {
content: "\e6ac";
}
.iconAudit:before {
content: "\e6a9";
}
.iconVerify:before {
content: "\e6aa";
}
.iconTransaction:before {
content: "\e6ab";
}
.iconEwm:before {
content: "\e6a6";
}
.iconIOS:before {
content: "\e6a7";
}
.iconAndroid:before {
content: "\e6a8";
}
.iconjdq_led_clean:before {
content: "\e67a";
}
.iconjdq_led_edit:before {
content: "\e67b";
}
.iconjdq_led_Led:before {
content: "\e680";
}
.iconjdq_led_Led1:before {
content: "\e681";
}
.iconjdq_led_Right:before {
content: "\e688";
}
.iconjdq_led_Ledjiesu:before {
content: "\e689";
}
.iconjdq_led_show:before {
content: "\e68a";
}
.iconjdq_led_Ledwx:before {
content: "\e68b";
}
.iconjdq_led_Lednrhg:before {
content: "\e68c";
}
.iconClean1:before {
content: "\e6a4";
}
.iconLoading:before {
content: "\e6a5";
}
.iconSubordinates:before {
content: "\e6a1";
}
.iconDownload:before {
content: "\e6a2";
}
.iconNext_Mission:before {
content: "\e6a3";
}
.iconAdmitted:before {
content: "\e69c";
}
.iconEmployment_Confirmation:before {
content: "\e69d";
}
.iconRepulsebeifen:before {
content: "\e69e";
}
.iconUpdate_Files:before {
content: "\e69f";
}
.iconRepulse:before {
content: "\e6a0";
}
.iconOverrule:before {
content: "\e69a";
}
.iconWithdrawn:before {
content: "\e69b";
}
.iconDetails:before {
content: "\e699";
}
.iconSpecial_Populations:before {
content: "\e698";
}
.iconPerson_Transfer:before {
content: "\e693";
}
.iconCreate_Files:before {
content: "\e694";
}
.iconPrint:before {
content: "\e695";
}
.iconPerson_Transfered:before {
content: "\e696";
}
.iconReject:before {
content: "\e697";
}
.iconGroup_IM:before {
content: "\e692";
}
.iconData_Screen:before {
content: "\e68f";
}
.iconCustomer_Service:before {
content: "\e690";
}
.iconDocumentation:before {
content: "\e691";
}
.iconTriangle_Up:before {
content: "\e68d";
}
.iconTriangle_Left:before {
content: "\e68e";
}
.iconDouble_Up:before {
content: "\e686";
}
.iconDouble_Down:before {
content: "\e687";
}
.iconServer:before {
content: "\e685";
}
.iconIM:before {
content: "\e682";
}
.iconStatistics:before {
content: "\e683";
}
.iconData_Reporting:before {
content: "\e684";
}
.iconMoveUp:before {
content: "\e67c";
}
.iconRevoke:before {
content: "\e67d";
}
.iconMoveDown:before {
content: "\e67e";
}
.iconAll_Profile:before {
content: "\e67f";
}
.iconzu:before {
content: "\e712";
}
.iconzu1:before {
content: "\e713";
}
.iconShow_Content:before {
content: "\e66e";
}
.iconShow:before {
content: "\e66f";
}
.iconCCP:before {
content: "\e70d";
}
.iconAlready_Read:before {
content: "\e669";
}
.iconRegister:before {
content: "\e66a";
}
.iconNotice:before {
content: "\e66b";
}
.iconUnfinished:before {
content: "\e66c";
}
.iconDate1:before {
content: "\e664";
}
.iconLogout:before {
content: "\e665";
}
.iconPhoto:before {
content: "\e666";
}
.iconResetting:before {
content: "\e667";
}
.iconProfile_Picture:before {
content: "\e668";
}
.iconModal_Success:before {
content: "\e662";
}
.iconModal_Warning:before {
content: "\e663";
}
.iconSteps_Finished:before {
content: "\e660";
}
.iconSteps_In_Progress:before {
content: "\e661";
}
.iconAdd_Subordinates:before {
content: "\e65d";
}
.iconAdd_Peers:before {
content: "\e65e";
}
.iconHide_Content:before {
content: "\e65f";
}
.iconLogo:before {
content: "\e65c";
}
.iconAdd:before {
content: "\e657";
}
.iconFunction_Management:before {
content: "\e658";
}
.iconDelete:before {
content: "\e659";
}
.iconExported:before {
content: "\e65a";
}
.iconImport:before {
content: "\e65b";
}
.iconTriangle_Down:before {
content: "\e655";
}
.iconTriangle_Right:before {
content: "\e656";
}
.iconNav_DataCenter:before {
content: "\e650";
}
.iconNav_Dashborad:before {
content: "\e651";
}
.iconNav_Pack_Up:before {
content: "\e652";
}
.iconNav_Setting:before {
content: "\e653";
}
.iconNav_Application:before {
content: "\e654";
}
.iconSkip:before {
content: "\e64a";
}
.iconSearch:before {
content: "\e64b";
}
.iconWeChat:before {
content: "\e64c";
}
.iconParent:before {
content: "\e64e";
}
.iconSetting:before {
content: "\e64f";
}
.iconChange:before {
content: "\e63c";
}
.iconBack_Large:before {
content: "\e63d";
}
.iconCorrect:before {
content: "\e63e";
}
.iconMessage:before {
content: "\e63f";
}
.iconDate:before {
content: "\e640";
}
.iconMore:before {
content: "\e641";
}
.iconEdit:before {
content: "\e642";
}
.iconPhone:before {
content: "\e643";
}
.iconClean:before {
content: "\e644";
}
.iconProlife:before {
content: "\e645";
}
.iconClock:before {
content: "\e646";
}
.iconLocation:before {
content: "\e647";
}
.iconPassword:before {
content: "\e648";
}
.iconCopy:before {
content: "\e649";
}
.iconArrow_Up:before {
content: "\e638";
}
.iconArrow_Left:before {
content: "\e639";
}
.iconArrow_Down:before {
content: "\e63a";
}
.iconArrow_Right:before {
content: "\e63b";
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,78 @@
@font-face {
font-family: "logofont"; /* Project id 1557923 */
src: url('//at.alicdn.com/t/c/font_1557923_ymu4rpvijlo.woff2?t=1661421638099') format('woff2'),
url('//at.alicdn.com/t/c/font_1557923_ymu4rpvijlo.woff?t=1661421638099') format('woff'),
url('//at.alicdn.com/t/c/font_1557923_ymu4rpvijlo.ttf?t=1661421638099') format('truetype');
}
.logofont {
font-family: "logofont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.iconhuizhili:before {
content: "\e720";
}
.iconxiuxingtong1:before {
content: "\e71e";
}
.iconhuizhengwu2:before {
content: "\e71a";
}
.iconhuizhengwu:before {
content: "\e719";
}
.iconxiuxingtong:before {
content: "\e717";
}
.iconzgydzhsq:before {
content: "\e6fb";
}
.iconzhongguoyidong2:before {
content: "\e6fa";
}
.iconzhongguoyidong:before {
content: "\e6f8";
}
.iconcunwei:before {
content: "\e6f3";
}
.iconcunwei1:before {
content: "\e6f0";
}
.iconccb:before {
content: "\e6d7";
}
.iconzhongzhi:before {
content: "\e6cb";
}
.iconzxjy:before {
content: "\e6c2";
}
.iconLogo:before {
content: "\e6a9";
}
.iconminzhengju:before {
content: "\e667";
}
.iconzhongguoliantong:before {
content: "\e618";
}

View File

@@ -0,0 +1,72 @@
$theme: "hzl";
@import "common";
.signLeftContent {
color: #333;
font-family: -apple-system, BlinkMacSystemFont, PingFang SC, Source Han Sans CN, Microsoft Yahei, sans-serif;
.titlePane {
margin-top: 84px;
margin-bottom: 40px;
font-family: MicrosoftYaHei, sans-serif;
}
.subTitle:before {
border-color: #333;
}
}
.projectName {
font-family: MicrosoftYaHeiS0pxibold;
font-size: 48px;
}
.ai-sign {
width: 420px !important;
& > .el-row--flex {
align-items: flex-start;
}
.is-always-shadow {
box-shadow: 0 24px 48px 0 rgba(15, 56, 139, 0.05);
width: 420px !important;
min-height: 430px;
& > .el-card__body {
padding: 20px 40px;
}
.ai-scan {
right: -30px;
.iconfont {
line-height: normal;
}
}
}
.reset-password-row {
text-align: center !important;
}
}
.headerNav {
.AiIcon {
font-size: 28px !important;
-webkit-text-fill-color: white !important;
font-weight: normal !important;
}
.headerTitle {
font-family: FZZZHONGJW--GB1-0 !important;
line-height: normal !important;
font-weight: normal !important;
-webkit-text-fill-color: white !important;
}
.textShadow {
display: none;
}
}

View File

@@ -0,0 +1,7 @@
$theme: "yellow";
$primaryColor: #f62;
$primaryBtnColor: linear-gradient(90deg, #FFA322 0%, #FF6622 100%);
$projectName: linear-gradient(180deg, #FFA322 0%, #FF6622 100%);
$primaryLightColor: #FFF7F4;
$textShadow: #CA693E;
@import "common";

12
ui/meta/styles/vars.scss Normal file
View File

@@ -0,0 +1,12 @@
$primaryColor: #26f !default;
$borderColor: #d0d4dc !default;
$primaryBtnColor: linear-gradient(90deg, #299FFF 0%, #0C61FF 100%) !default;
$successColor: #2EA222 !default;
$warnColor: #F82 !default;
$errorColor: #F46 !default;
$infoColor: #8D96A9 !default;
$theme: "blue" !default;
$projectName: linear-gradient(180deg, #5AC4FF 11%, #1347B6 100%) !default;
$primaryLightColor: rgba(239, 246, 255, 1) !default;
$textShadow: #384DC3 !default;
$placeholderColor: #888 !default;

59
ui/package.json Normal file
View File

@@ -0,0 +1,59 @@
{
"name": "dui",
"version": "2.0.0",
"private": false,
"author": "kubbo",
"scripts": {
"build": "node ../bin/ui.js&&vue-cli-service build --no-clean --target lib --dest lib packages/index.js",
"lib": "npm run build&&npm unpublish --force&&npm publish"
},
"files": [
"lib"
],
"main": "lib/dui.common.js",
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@ckeditor/ckeditor5-vue2": "^3.0.1",
"@jiaminghi/data-view": "^2.10.0",
"axios": "^0.19.2",
"dayjs": "^1.8.35",
"echarts": "^5.1.2",
"v-viewer": "^1.5.1",
"vue-cropper": "^0.5.5",
"vue-qr": "^2.2.1",
"vuedraggable": "^2.24.3"
},
"peerDependencies": {
"element-ui": "^2.13.2",
"vue": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"web-types": "docs/web-types.json",
"vetur": {
"tags": "docs/tags.json",
"attributes": "docs/attributes.json"
}
}

View File

@@ -0,0 +1,331 @@
<template>
<div class="ai-article">
<div v-html="value"></div>
</div>
</template>
<script>
export default {
name: 'AiArticle',
props: {
value: {
type: String
}
}
}
</script>
<style lang="scss" scoped>
.ai-article {
width: 100%;
line-height: 1.75;
font-weight: 400;
color: #333;
font-size: 14px;
text-align: justify;
overflow-x: auto;
word-break: break-word;
::v-deep h1 {
margin: 1.3rem 0;
line-height: 1.2
}
::v-deep p {
line-height: 2.27rem
}
::v-deep hr {
border: none;
border-top: 1px solid #ddd;
margin-top: 2.7rem;
margin-bottom: 2.7rem
}
::v-deep img:not(.equation), ::v-deep iframe, ::v-deep embed, ::v-deep video {
display: block;
margin: 18px auto;
max-width: 100% !important;
}
::v-deep img.equation {
margin: 0 .1em;
max-width: 100% !important;
vertical-align: middle
}
::v-deep figure {
margin: 2.7rem auto;
text-align: center
}
::v-deep img:not(.equation) {
cursor: zoom-in
}
::v-deep figure figcaption {
text-align: center;
font-size: 1rem;
line-height: 2.7rem;
color: #909090
}
::v-deep pre {
line-height: 1.93rem;
overflow: auto
}
::v-deep code,
::v-deep pre {
font-family: Menlo, Monaco, Consolas, Courier New, monospace
}
::v-deep code {
font-size: 1rem;
padding: .26rem .53em;
word-break: break-word;
color: #4e5980;
background-color: #f8f8f8;
border-radius: 2px;
overflow-x: auto
}
::v-deep pre>code {
font-size: 1rem;
padding: .67rem 1.3rem;
margin: 0;
word-break: normal;
display: block
}
::v-deep a {
color: #259
}
::v-deep a:active,
::v-deep a:hover {
color: #275b8c
}
::v-deep table {
width: 100%;
margin-top: 18px;
margin-bottom: 18px;
overflow: auto;
font-size: 1rem;
text-align: center;
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
}
::v-deep thead {
background: #f6f6f6;
color: #000;
text-align: left
}
::v-deep td,
::v-deep th {
padding: 3px 5px;
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
}
::v-deep td {
min-width: 10rem
}
::v-deep blockquote {
margin: 1em 0;
border-left: 4px solid #ddd;
padding: 0 1.3rem
}
::v-deep blockquote>p {
margin: .6rem 0
}
::v-deep ol,
::v-deep ul {
padding-left: 2.7rem
}
::v-deep ol li,
::v-deep ul li {
margin-bottom: .6rem
}
::v-deep ol ol,
::v-deep ol ul,
::v-deep ul ol,
::v-deep ul ul {
margin-top: .27rem
}
::v-deep pre>code {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
color: #333;
background: #f8f8f8
}
::v-deep p {
line-height: inherit;
margin-top: 18px;
margin-bottom: 18px
}
::v-deep img {
max-height: none
}
::v-deep a {
color: #0269c8;
border-bottom: 1px solid #d1e9ff
}
::v-deep code {
background-color: #fff5f5;
color: #ff502c;
font-size: .87em;
padding: .065em .4em
}
::v-deep blockquote {
color: #666;
padding: 1px 23px;
margin: 18px 0;
border-left: 4px solid #cbcbcb;
background-color: #f8f8f8
}
::v-deep blockquote:after {
display: block;
content: ""
}
::v-deep blockquote>p {
margin: 10px 0
}
::v-deep blockquote.warning {
position: relative;
border-left-color: #f75151;
margin-left: 8px
}
::v-deep blockquote.warning:before {
position: absolute;
top: 14px;
left: -12px;
background: #f75151;
border-radius: 50%;
content: "!";
width: 20px;
height: 20px;
color: #fff;
display: flex;
align-items: center;
justify-content: center
}
::v-deep ol,
::v-deep ul {
padding-left: 28px
}
::v-deep ol li,
::v-deep ul li {
margin-bottom: 0;
list-style: inherit
}
::v-deep ol li.task-list-item,
::v-deep ul li.task-list-item {
list-style: none
}
::v-deep ol li.task-list-item ol,
::v-deep ol li.task-list-item ul,
::v-deep ul li.task-list-item ol,
::v-deep ul li.task-list-item ul {
margin-top: 0
}
::v-deep ol li {
padding-left: 6px
}
::v-deep pre {
position: relative;
line-height: 1.75
}
::v-deep pre>code {
padding: 15px 12px
}
::v-deep pre>code.hljs[lang] {
padding: 18px 15px 12px
}
::v-deep h1,
::v-deep h2,
::v-deep h3,
::v-deep h4,
::v-deep h5,
::v-deep h6 {
color: #333;
line-height: 1.5;
margin-top: 35px;
margin-bottom: 10px;
padding-bottom: 5px;
font-weight: 500;
}
::v-deep h1 {
font-size: 30px;
margin-bottom: 5px
}
::v-deep h2 {
padding-bottom: 12px;
font-size: 24px;
border-bottom: 1px solid #ececec
}
::v-deep h3 {
font-size: 18px;
padding-bottom: 0
}
::v-deep h4 {
font-size: 16px
}
::v-deep h5 {
font-size: 15px
}
::v-deep h6 {
margin-top: 5px
}
::v-deep h1.heading+h2.heading {
margin-top: 20px
}
::v-deep h1.heading+h3.heading {
margin-top: 15px
}
::v-deep .heading+.heading {
margin-top: 0
}
::v-deep h1+:not(.heading) {
margin-top: 25px
}
}
</style>

View File

@@ -0,0 +1,238 @@
<template>
<section class="ai-audio">
<div class="controller" :class="skinClassName">
<el-row :gutter="10" type="flex">
<el-col>
<i class="play-icon" :class="playIcon" @click="play"></i>
</el-col>
<el-col class="play-progress">
<el-slider
:format-tooltip="getDateFormat"
v-model="currentTime"
:max="totalDuration"
@change="audio.currentTime = currentTime">
</el-slider>
<span class="total" v-if="skin === 'flat'">{{ getDateFormat(currentTime) }}</span>
<span class="total" v-else>{{ [getDateFormat(currentTime), getDateFormat(totalDuration)].join('/') }}</span>
</el-col>
</el-row>
<audio
ref="audio"
style="display: none;"
:src="src"
preload="metadata"
@pause="playState = false"
@playing="playing"
@loadedmetadata="loadedmetadata"
@timeupdate="timeupdate"
></audio>
</div>
</section>
</template>
<script>
import moment from 'dayjs'
export default {
name: 'AiAudio',
props: {
/**
* 播放资源url
*/
src: {
type: String,
required: true,
},
/**
* 同时只允许一个播放
*/
singlePlay: {type: Boolean, default: false},
/**
* 播放器风格
* default 旧版样式,flat 新版样式
* @values default,flat
*/
skin: {
type: String,
default: 'default'
}
},
data() {
return {
audio: null,
playState: false,
currentTime: 0,
totalTime: 0
}
},
computed: {
playIcon() {
if (this.skin === 'flat') {
return this.playState ? 'iconfont iconMediaPlayer_Stop' : 'iconfont iconMediaPlayer_Play'
}
return this.playState ? 'el-icon-video-pause' : 'el-icon-video-play'
},
totalDuration() {
return Math.round(this.totalTime)
},
skinClassName() {
return this.skin === 'default' ? '' : this.skin
}
},
mounted() {
this.audio = this.$refs.audio
},
methods: {
play() {
if (this.audio) {
if (this.audio.paused) {
if (this.singlePlay) this.stopAllAudio()
this.audio.play()
} else {
this.audio.pause()
}
}
},
stopAllAudio() {
let audios = document.getElementsByTagName('audio')
for (let i = 0; i < audios.length; i++) {
if (audios[i]) audios[i].pause()
}
},
playing() {
this.playState = true
},
loadedmetadata() {
this.totalTime = this.$refs.audio.duration || 0
},
getDateFormat(val) {
const time = moment.duration(val, 'seconds')
return [
this.prefixNum(time.minutes(), 2),
this.prefixNum(time.seconds(), 2),
].join(':')
},
prefixNum(val, num) {
return (Array(num).join('0') + val).slice(-num)
},
timeupdate() {
this.currentTime = parseInt(this.audio.currentTime)
}
},
destroyed() {
this.audio.pause()
}
}
</script>
<style lang="scss" scoped>
.ai-audio {
display: flex;
flex: 1;
.controller {
width: 205px;
height: 40px;
padding: 0 5px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 99px;
.el-col {
width: auto;
}
.play-icon {
color: #4c84ff;
cursor: pointer;
line-height: 40px;
font-size: 32px;
transition: color 0.3s;
&:hover {
color: #b3d8ff;
}
}
.play-progress {
display: flex;
flex: 1;
.el-slider {
width: 60px;
}
.total {
margin-left: 15px;
font-size: 12px;
line-height: 40px;
}
}
}
.flat {
height: 32px;
background: rgba(239, 246, 255, 1);
border: 1px solid rgba(132, 181, 255, 1);
border-radius: 5px;
box-shadow: none;
.play-icon {
position: relative;
padding-left: 3px;
color: #4c84ff;
cursor: pointer;
line-height: 32px;
font-size: 20px;
transition: color 0.3s;
&:hover {
color: #b3d8ff;
}
}
::v-deep.el-slider__button {
width: 8px;
height: 8px;
background: #1365DD;
}
::v-deep.el-slider__bar {
height: 2px;
}
::v-deep.el-slider__button-wrapper {
height: 32px;
}
::v-deep.el-slider__runway {
height: 2px;
margin: 15px 0;
}
.play-progress {
display: flex;
.el-slider {
width: 84px;
}
.total {
margin-left: 8px;
font-size: 12px;
line-height: 32px;
}
}
}
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<section class="AiBadge">
<slot></slot>
<sup v-if="isShow" class="badge">
<span v-if="badge">{{badge}}</span>
<slot v-else name="badge"></slot>
</sup>
</section>
</template>
<script>
export default {
name: "AiBadge",
props: {
badge: String,
isShow: {type: Boolean, default: true}
}
}
</script>
<style lang="scss" scoped>
.AiBadge {
.badge {
position: absolute;
margin-left: -10px;
margin-top: -10px;
}
}
</style>

117
ui/packages/basic/AiBar.vue Normal file
View File

@@ -0,0 +1,117 @@
<template>
<div class="aibar" :style="{ marginBottom: marginBottom }" :class="[titlePosition === 'center' ? 'aibar-center' : '']">
<div v-if="titlePosition === 'center'"></div>
<div class="aibar-left" :class="[titlePosition === 'center' ? 'aibar-left__center' : '']">
<template v-if="!isHasTitleSlot">{{ title }}</template>
<slot name="title" v-else></slot>
</div>
<div class="aibar-right">
<slot name="right"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'AiBar',
props: {
title: {
type: String
},
customCliker: {
type: Boolean,
default: false
},
marginBottom: {
type: String,
default: '16px'
},
titlePosition: {
type: String,
default: 'left'
}
},
computed: {
isHasTitleSlot () {
return this.$slots.title
}
},
data () {
return {
}
}
}
</script>
<style lang="scss" scoped>
.aibar {
display: flex;
position: relative;
align-items: center;
justify-content: space-between;
height: 56px;
padding: 0 16px;
box-sizing: border-box;
border-bottom: 1px solid #EEEEEE;
.aibar-left {
color: #222;
font-size: 16px;
font-weight: 700;
}
.aibar-left__center {
position: relative;
width: 556px;
text-align: center;
word-break: break-all;
line-height: 24px;
}
.aibar-right {
display: flex;
align-items: center;
color: #5088FF;
font-size: 12px;
i {
line-height: 1;
color: #5088FF;
}
span {
font-size: 12px;
}
& > div, & > a {
display: flex;
align-items: center;
margin-right: 20px;
&:last-child {
margin-right: 0;
}
}
}
}
.aibar-center {
height: auto;
padding: 10px 0;
h2 {
margin: 0 0 10px 0;
}
p {
color: #888;
font-size: 14px;
}
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<section class="AiBigTable"/>
</template>
<script>
import AiEmpty from "./AiEmpty";
export default {
name: "AiBigTable",
components: {AiEmpty},
model: {
prop: "data",
event: "input"
},
props: {
/**
* 表数据
*/
data: {default: () => []},
/**
* 表格配置
*/
colConfigs: {default: () => []},
/**
* 是否显示边框
*/
border: Boolean,
/**
* 是否启用dom对象
*/
isDom: Boolean
},
data() {
return {
html2canvas: null
}
},
methods: {
renderTable() {
const table = document.createElement("div")
table.style.display = 'flex'
table.style.flexDirection = 'column'
table.appendChild(this.renderHead())
if (this.data.length > 0) {
this.data.map(e => table.appendChild(this.renderRow(e)))
} else table.appendChild(this.renderEmpty())
if (this.isDom) {
this.$el.appendChild(table)
} else {
this.$el.appendChild(table)
this.$load(this.html2canvas).then(() => this.html2canvas(table, {
allowTaint: true,
useCORS: true,
height: this.$el.offsetHeight,
width: this.$el.offsetWidth
})).then(ctx => {
this.$el.removeChild(table)
this.$el.appendChild(ctx)
})
}
},
renderHead() {
const head = document.createElement("div")
head.style.display = 'flex'
head.style.alignItems = 'center'
head.style.fontWeight = 'bold'
head.style.background = 'rgba(243, 246, 249, 1)'
if (this.border) {
head.style.borderLeft = '1px solid #eee'
head.style.borderTop = '1px solid #eee'
} else {
head.style.borderBottom = '1px solid #eee'
}
this.colConfigs.map(e => {
const cell = this.renderCell(e)
head.appendChild(cell)
})
return head
},
renderRow(item) {
const row = document.createElement("div")
row.style.display = 'flex'
row.style.alignItems = 'center'
if (this.border) {
row.style.borderLeft = '1px solid #eee'
} else {
row.style.borderBottom = '1px solid #eee'
}
this.colConfigs.map(e => {
const cell = this.renderCell(e, item)
row.appendChild(cell)
})
return row
},
renderCell(config, row) {
const cell = document.createElement("div")
cell.style.display = 'flex'
cell.style.alignItems = 'center'
cell.style.minheight = '32px'
cell.style.padding = '0 8px'
if (this.border) {
cell.style.borderBottom = '1px solid #eee'
cell.style.borderRight = '1px solid #eee'
}
if (config.align) {
cell.style.justifyContent = config.align
}
if (config.width) {
cell.style.width = config.width.toString().replace(/(\d+)/g, '$1px')
cell.style.flexShrink = 0
} else {
cell.style.flex = 1
cell.style.minWidth = 0
}
cell.innerHTML = row?.[config.prop] || config.label
return cell
},
renderEmpty() {
const empty = document.createElement("div")
empty.style.background = 'url("https://cdn.cunwuyun.cn/dvcp/empty.svg") no-repeat'
empty.style.width = '100%'
empty.style.height = '140px'
empty.style.backgroundPosition = 'center 0'
empty.style.backgroundSize = '120px'
empty.style.borderBottom = '1px solid #eee'
empty.style.borderLeft = '1px solid #eee'
empty.style.borderRight = '1px solid #eee'
empty.style.textAlign = 'center'
empty.style.color = '#999'
empty.style.paddingTop = '110px'
empty.innerHTML = "暂无数据"
return empty
}
},
mounted() {
this.$injectLib("https://cdn.cunwuyun.cn/html2canvas.min.js", () => {
this.html2canvas = window?.html2canvas
this.$nextTick(this.renderTable)
})
}
}
</script>
<style lang="scss" scoped>
.AiBigTable {
flex: 1;
min-width: 0;
min-height: 0;
}
</style>

View File

@@ -0,0 +1,522 @@
<template>
<section class="AiCron">
<div id="changeContab">
<el-tabs type="border-card">
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i></span>
<div class="tabBody">
<el-row>
<el-radio v-model="second.cronEvery" label="1">每一秒中</el-radio>
</el-row>
<el-row>
<el-radio v-model="second.cronEvery" label="2">每隔
<el-input-number size="small" v-model="second.incrementIncrement" :min="1" :max="60"></el-input-number>
秒执行
<el-input-number size="small" v-model="second.incrementStart" :min="0" :max="59"></el-input-number>
秒开始
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="second.cronEvery" label="3">具体秒数(可多选)
<el-select size="small" multiple v-model="second.specificSpecific">
<el-option v-for="(val,i) in 60" :key="i" :value="val-1">{{val-1}}</el-option>
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="second.cronEvery" label="4">周期从
<el-input-number size="small" v-model="second.rangeStart" :min="1" :max="60"></el-input-number>
<el-input-number size="small" v-model="second.rangeEnd" :min="0" :max="59"></el-input-number>
</el-radio>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i></span>
<div class="tabBody">
<el-row>
<el-radio v-model="minute.cronEvery" label="1">每一分钟</el-radio>
</el-row>
<el-row>
<el-radio v-model="minute.cronEvery" label="2">每隔
<el-input-number size="small" v-model="minute.incrementIncrement" :min="1" :max="60"></el-input-number>
分执行
<el-input-number size="small" v-model="minute.incrementStart" :min="0" :max="59"></el-input-number>
分开始
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="minute.cronEvery" label="3">具体分钟数(可多选)
<el-select size="small" multiple v-model="minute.specificSpecific">
<el-option v-for="(val,i) in 60" :key="i" :value="val-1">{{val-1}}</el-option>
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="minute.cronEvery" label="4">周期从
<el-input-number size="small" v-model="minute.rangeStart" :min="1" :max="60"></el-input-number>
<el-input-number size="small" v-model="minute.rangeEnd" :min="0" :max="59"></el-input-number>
</el-radio>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i></span>
<div class="tabBody">
<el-row>
<el-radio v-model="hour.cronEvery" label="1">每一小时</el-radio>
</el-row>
<el-row>
<el-radio v-model="hour.cronEvery" label="2">每隔
<el-input-number size="small" v-model="hour.incrementIncrement" :min="0" :max="23"></el-input-number>
小时执行
<el-input-number size="small" v-model="hour.incrementStart" :min="0" :max="23"></el-input-number>
小时开始
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="hour.cronEvery" label="3">具体小时数(可多选)
<el-select size="small" multiple v-model="hour.specificSpecific">
<el-option v-for="(val,i) in 24" :key="i" :value="val-1">{{val-1}}</el-option>
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="hour.cronEvery" label="4">周期从
<el-input-number size="small" v-model="hour.rangeStart" :min="0" :max="23"></el-input-number>
<el-input-number size="small" v-model="hour.rangeEnd" :min="0" :max="23"></el-input-number>
小时
</el-radio>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i></span>
<div class="tabBody">
<el-row>
<el-radio v-model="day.cronEvery" label="1">每一天</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="2">每隔
<el-input-number size="small" v-model="week.incrementIncrement" :min="1" :max="7"></el-input-number>
周执行
<el-select size="small" v-model="week.incrementStart">
<el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option>
</el-select>
开始
</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="3">每隔
<el-input-number size="small" v-model="day.incrementIncrement" :min="1" :max="31"></el-input-number>
天执行
<el-input-number size="small" v-model="day.incrementStart" :min="1" :max="31"></el-input-number>
天开始
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="day.cronEvery" label="4">具体星期几(可多选)
<el-select size="small" multiple v-model="week.specificSpecific">
<el-option v-for="(val,i) in 7"
:key="i"
:label="weeks[val-1]"
:value="['SUN','MON','TUE','WED','THU','FRI','SAT'][val-1]"
></el-option>
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="day.cronEvery" label="5">具体天数(可多选)
<el-select size="small" multiple v-model="day.specificSpecific">
<el-option v-for="(val,i) in 31" :key="i" :value="val">{{val}}</el-option>
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="6">在这个月的最后一天</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="7">在这个月的最后一个工作日</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="8">在这个月的最后一个
<el-select size="small" v-model="day.cronLastSpecificDomDay">
<el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option>
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="9">
<el-input-number size="small" v-model="day.cronDaysBeforeEomMinus" :min="1" :max="31"></el-input-number>
在本月底前
</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="10">最近的工作日(周一至周五) 至本月
<el-input-number size="small" v-model="day.cronDaysNearestWeekday" :min="1" :max="31"></el-input-number>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="11">在这个月的第
<el-input-number size="small" v-model="week.cronNthDayNth" :min="1" :max="5"></el-input-number>
<el-select size="small" v-model="week.cronNthDayDay">
<el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option>
</el-select>
</el-radio>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> </span>
<div class="tabBody">
<el-row>
<el-radio v-model="month.cronEvery" label="1">每个月</el-radio>
</el-row>
<el-row>
<el-radio v-model="month.cronEvery" label="2">每隔
<el-input-number size="small" v-model="month.incrementIncrement" :min="0" :max="12"></el-input-number>
月执行
<el-input-number size="small" v-model="month.incrementStart" :min="0" :max="12"></el-input-number>
月开始
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="month.cronEvery" label="3">具体月数(可多选)
<el-select size="small" multiple v-model="month.specificSpecific">
<el-option v-for="(val,i) in 12" :key="i" :label="val" :value="val"></el-option>
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="month.cronEvery" label="4">
<el-input-number size="small" v-model="month.rangeStart" :min="1" :max="12"></el-input-number>
<el-input-number size="small" v-model="month.rangeEnd" :min="1" :max="12"></el-input-number>
</el-radio>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
<div class="bottom">
<span class="value">{{cron}}</span>
<el-button type="primary" @click="change">保存</el-button>
<el-button type="primary" @click="close">取消</el-button>
</div>
</div>
</section>
</template>
<script>
export default {
name: "AiCron",
model: {
prop: 'value',
event: "change"
},
props: {
value: String
},
data() {
return {
weeks: ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天"],
second: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: [],
},
minute: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: [],
},
hour: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: [],
},
day: {
cronEvery: '',
incrementStart: '1',
incrementIncrement: '1',
rangeStart: '',
rangeEnd: '',
specificSpecific: [],
cronLastSpecificDomDay: 1,
cronDaysBeforeEomMinus: '',
cronDaysNearestWeekday: '',
},
week: {
cronEvery: '',
incrementStart: '1',
incrementIncrement: '1',
specificSpecific: [],
cronNthDayDay: 1,
cronNthDayNth: '1',
},
month: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: [],
},
output: {
second: '',
minute: '',
hour: '',
day: '',
month: '',
Week: '',
},
visible: false,
cronArr:['*','*','*','*','*','?']
}
},
watch: {
cron(val) {
this.value = val
}
},
computed: {
secondsText() {
let seconds = this.cronArr[0];
let cronEvery = this.second.cronEvery;
switch (cronEvery.toString()) {
case '1':
seconds = '*';
break;
case '2':
seconds = this.second.incrementStart + '/' + this.second.incrementIncrement;
break;
case '3':
this.second.specificSpecific.map(val => {
seconds += val + ','
});
seconds = seconds.slice(0, -1);
break;
case '4':
seconds = this.second.rangeStart + '-' + this.second.rangeEnd;
break;
}
return seconds;
},
minutesText() {
let minutes = this.cronArr[2];;
let cronEvery = this.minute.cronEvery;
switch (cronEvery.toString()) {
case '1':
minutes = '*';
break;
case '2':
minutes = this.minute.incrementStart + '/' + this.minute.incrementIncrement;
break;
case '3':
this.minute.specificSpecific.map(val => {
minutes += val + ','
});
minutes = minutes.slice(0, -1);
break;
case '4':
minutes = this.minute.rangeStart + '-' + this.minute.rangeEnd;
break;
}
return minutes;
},
hoursText() {
let hours = this.cronArr[2];;
let cronEvery = this.hour.cronEvery;
switch (cronEvery.toString()) {
case '1':
hours = '*';
break;
case '2':
hours = this.hour.incrementStart + '/' + this.hour.incrementIncrement;
break;
case '3':
this.hour.specificSpecific.map(val => {
hours += val + ','
});
hours = hours.slice(0, -1);
break;
case '4':
hours = this.hour.rangeStart + '-' + this.hour.rangeEnd;
break;
}
return hours;
},
daysText() {
let days = this.cronArr[3];;
let cronEvery = this.day.cronEvery;
switch (cronEvery.toString()) {
case '1':
break;
case '2':
case '4':
case '11':
days = '?';
break;
case '3':
days = this.day.incrementStart + '/' + this.day.incrementIncrement;
break;
case '5':
this.day.specificSpecific.map(val => {
days += val + ','
});
days = days.slice(0, -1);
break;
case '6':
days = "L";
break;
case '7':
days = "LW";
break;
case '8':
days = this.day.cronLastSpecificDomDay + 'L';
break;
case '9':
days = 'L-' + this.day.cronDaysBeforeEomMinus;
break;
case '10':
days = this.day.cronDaysNearestWeekday + "W";
break
}
return days;
},
weeksText() {
let weeks = this.cronArr[5];;
let cronEvery = this.day.cronEvery;
switch (cronEvery.toString()) {
case '1':
case '3':
case '5':
weeks = '?';
break;
case '2':
weeks = this.week.incrementStart + '/' + this.week.incrementIncrement;
break;
case '4':
this.week.specificSpecific.map(val => {
weeks += val + ','
});
weeks = weeks.slice(0, -1);
break;
case '6':
case '7':
case '8':
case '9':
case '10':
weeks = "?";
break;
case '11':
weeks = this.week.cronNthDayDay + "#" + this.week.cronNthDayNth;
break;
}
return weeks;
},
monthsText() {
let months = this.cronArr[4];
let cronEvery = this.month.cronEvery;
switch (cronEvery.toString()) {
case '1':
months = '*';
break;
case '2':
months = this.month.incrementStart + '/' + this.month.incrementIncrement;
break;
case '3':
this.month.specificSpecific.map(val => {
months += val + ','
});
months = months.slice(0, -1);
break;
case '4':
months = this.month.rangeStart + '-' + this.month.rangeEnd;
break;
}
return months;
},
cron() {
return `${this.secondsText || '*'} ${this.minutesText || '*'} ${this.hoursText || '*'} ${this.daysText || '*'} ${this.monthsText || '*'} ${this.weeksText || '?'} `
},
},
methods: {
getValue() {
return this.cron;
},
change() {
this.$emit('change', this.cron);
this.close();
},
close() {
this.$emit('close')
},
},
mounted() {
if(this.value) this.cronArr = this.value.trim().split(" ")
}
}
</script>
<style lang="scss" scoped>
.AiCron {
#changeContab {
.language {
position: absolute;
right: 25px;
z-index: 1;
}
.el-tabs {
box-shadow: none;
}
.tabBody {
.el-row {
margin: 10px 0;
.long {
.el-select {
width: 350px;
}
}
.el-input-number {
width: 110px;
}
}
}
.bottom {
width: 100%;
text-align: center;
margin-top: 5px;
position: relative;
.value {
font-size: 18px;
vertical-align: middle;
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<section class="ai-dialog__wrapper">
<el-dialog
custom-class="ai-dialog"
v-on="$listeners"
v-if="isEmpty"
:close-on-click-modal="closeOnClickModal"
v-bind="$attrs"
:destroy-on-close="destroyOnClose"
:visible.sync="dialogVisible">
<div class="ai-dialog__header" slot="title">
<h2>{{ title }}</h2>
</div>
<div class="ai-dialog__content" :style="{'max-height': isScrool ? '500px' : 'auto'}">
<div class="ai-dialog__content--wrapper" :style="{'padding-right': isScrool ? '8px' : '0'}">
<slot></slot>
</div>
</div>
<template v-if="customFooter" slot="footer">
<slot name="footer"></slot>
</template>
<div v-else class="dialog-footer" slot="footer">
<el-button @click="onCancel">取消</el-button>
<el-button @click="onConfirm" type="primary" style="width: 92px;">确认</el-button>
</div>
</el-dialog>
</section>
</template>
<script>
export default {
name: 'AiDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
customFooter: {
type: Boolean,
default: false
},
'close-on-click-modal': {
type: Boolean,
default: false
},
destroyOnClose: {
type: Boolean,
default: true
}
},
data () {
return {
dialogVisible: false,
isScrool: true,
isEmpty: true
}
},
watch: {
visible: {
handler (val) {
this.dialogVisible = val
// if (val) {
// this.$nextTick(() => {
// setTimeout(() => {
// this.isScrool = document.querySelector('.ai-dialog__content') && document.querySelector('.ai-dialog__content').clientHeight >= 500
// }, 100)
// })
// }
if (this.destroyOnClose && !val) {
setTimeout(() => {
this.isEmpty = false
setTimeout(() => {
this.isEmpty = true
}, 50)
}, 500)
}
}
}
},
mounted () {
},
methods: {
onCancel () {
this.$emit('update:visible', false)
this.$emit('onCancel')
},
onConfirm () {
this.$emit('onConfirm')
}
}
}
</script>
<style lang="scss">
.ai-dialog {
margin: 0!important;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.el-dialog__body {
padding: 20px 40px 20px;
}
.ai-dialog__content {
overflow-y: auto;
padding-bottom: 4px;
.ai-dialog__content--wrapper {
height: 100%;
overflow-x: hidden;
overflow-y: overlay;
}
}
.ai-dialog__header {
height: 48px;
line-height: 48px;
padding: 0 16px;
border-bottom: 1px solid #eee;
h2 {
font-size: 16px;
font-weight: 700;
}
}
.el-dialog__footer {
padding: 16px 20px;
box-sizing: border-box;
background: #F3F6F9;
text-align: center;
& + .el-button {
margin-left: 8px;
}
.el-button {
width: 92px!important;
}
}
.el-dialog__header {
padding: 0;
}
.el-dialog__headerbtn {
top: 24px;
transform: translateY(-50%);
}
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<section class="AiDrawer" :style="{width:`${width}px`,height:`${height}px`}">
<slot name="tools"/>
<div class="resetSignature" @click.stop="handleClean">
<ai-icon icon="iconResetting"/>
<span>重写</span>
</div>
<div v-if="drawPlaceholder" class="placeholder">{{ placeholder }}</div>
<canvas id="drawer" :width="width" :height="height" @mousedown.stop="handleDrawStart($event)"
@mouseup.stop="handleDrawEnd" @mouseleave="handleDrawEnd"
@mousemove.stop="handleDrawing($event)"
@touchstart.stop="handleDrawStart" @touchend.stop="handleDrawEnd" @touchmove="handleDrawing"/>
</section>
</template>
<script>
export default {
name: "AiDrawer",
props: {
seal: String,
placeholder: {type: String, default: "请在此处清晰书写你的签名"},
width: {type: Number, default: 640},
height: {type: Number, default: 480}
},
data() {
return {
drawing: false,//判断是否处于绘画中
drawer: {},
drawPlaceholder: true
}
},
watch: {
drawing() {
this.drawPlaceholder = false
}
},
mounted() {
this.initDrawer()
},
methods: {
initDrawer() {
this.$nextTick(() => {
let canvas = document.querySelector("#drawer")
if (canvas) {
this.drawer = canvas?.getContext("2d")
this.drawer.lineWidth = 3
this.drawer.shadowColor = '#333'
this.drawer.strokeStyle = '#333'
this.drawer.shadowBlur = 2
}
})
},
handleDrawStart(e) {
this.drawing = true
const canvasX = e.offsetX
const canvasY = e.offsetY
this.drawer?.beginPath()
this.drawer?.moveTo(canvasX, canvasY)
},
handleDrawEnd() {
this.drawing = false
this.$emit("update:seal", this.drawer.canvas.toDataURL("image/png", 1))
},
handleDrawing(e) {
if (this.drawing) {
const t = e.target?.getBoundingClientRect()
let canvasX
let canvasY
// 根据触发事件类型来进行定位
if (e.type == "touchmove") {
canvasX = e.changedTouches[0].clientX - t.x
canvasY = e.changedTouches[0].clientY - t.y
} else {
canvasX = e.offsetX
canvasY = e.offsetY
}
// 连接到移动的位置并上色
this.drawer?.lineTo(canvasX, canvasY)
this.drawer?.stroke()
}
},
handleClean() {
this.drawer.clearRect(0, 0, this.drawer.canvas.width, this.drawer.canvas.height)
this.drawPlaceholder = true
}
}
}
</script>
<style lang="scss" scoped>
.AiDrawer {
margin: 0 40px;
background: #F7F7F7;
border: 1px solid #CCCCCC;
position: relative;
#drawer {
display: block;
cursor: crosshair;
}
.resetSignature {
position: absolute;
width: 76px;
height: 32px;
background: rgba(#000, .5);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: rgba(#fff, .6);
cursor: pointer;
right: 16px;
top: 16px;
.AiIcon {
width: auto;
height: auto;
}
&:hover {
color: #fff;
}
}
.placeholder {
pointer-events: none;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 407px;
text-align: center;
font-size: 64px;
color: #DDDDDD;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<section class="AiEditor" :class="{invalid:valid&&!validateState}">
<ck-editor v-if="sourceEditor" :editor="sourceEditor" v-model="content" :config="editorConfig" @ready="handleReady" @input="$emit('change',repairContent)"/>
</section>
</template>
<script>
/**
* 原组件ckeditor封装
* 修改者:Kubbo
*/
import CKEditor from '@ckeditor/ckeditor5-vue2'
import {UploadAdapter} from "./models";
export default {
name: "AiEditor",
components: {ckEditor: CKEditor.component},
inject: {
elFormItem: {default: ""},
elForm: {default: ''},
},
model: {
prop: "value",
event: "change"
},
props: {
value: {type: String, required: true, default: ""},
placeholder: {type: String, default: '请输入正文'},
conf: Object,
instance: {type: Function, required: true},
valid: {type: Boolean, default: true},
text: {default: ""},
action: {default: "/admin/file/add"},
params: {default: () => ({})},
mode: {default: "default"}
},
data() {
return {
sourceEditor: null,
editor: null,
isPasteStyle: true,//粘贴是否携带格式
content: ""
}
},
computed: {
validateState() {
return ['', 'success'].includes(this.elFormItem?.validateState)
},
editorConfig() {
return {
mediaEmbed: {
previewsInData: true
},
htmlSupport: {
allow: [{name: /[\s\S]+/, styles: true, classes: true, attributes: true}]
},
...this.conf
}
},
repairContent: v => `<section class="ck-content">${v.content?.replace(/\s0w"/g, '"')?.replace(/srcset/g, 'src')}</section>`
},
watch: {
content(v) {
v && this.dispatch('ElFormItem', 'el.form.change', [v])
}
},
methods: {
handleReady(editor) {
this.editor = editor
const {instance, action, params} = this
editor.plugins.get('FileRepository').createUploadAdapter = loader => new UploadAdapter(loader, instance, action, params)
},
/**
* 表单验证
* @param componentName
* @param eventName
* @param params
*/
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
initValue() {
let unwatch = this.$watch('value', (v) => {
const init = v.replace(/<section class="ck-content">(.*)<\/section>/, '$1')
if (!!this.content) unwatch && unwatch()
else if (!!init) {
this.content = init
unwatch && unwatch()
}
}, {immediate: true})
},
loadEditor(count = 0) {
if (!!window?.ClassicEditor) {
this.sourceEditor = ClassicEditor
this.initValue()
} else if (count < 10) {
setTimeout(() => this.loadEditor(++count), 50)
} else {
console.error("无法加载编辑器资源,请联系管理员")
}
}
},
created() {
this.$injectLib("https://cdn.cunwuyun.cn/ckeditor.js", () => {
this.loadEditor()
})
}
}
</script>
<style lang="scss" scoped>
.AiEditor {
::v-deep.ck-content {
min-height: 300px;
}
}
</style>

View File

@@ -0,0 +1,24 @@
import {Loading} from "element-ui"
export class UploadAdapter {
constructor(loader, instance, action, params) {
this.instance = instance
this.action = action
this.loader = loader
this.params = params
}
async upload() {
const formData = new FormData()
formData.append('file', await this.loader.file)
const loading = Loading.service({})
return this.instance.post(this.action, formData, {...this.params, returnError: true}).then(res => {
if (res?.data) {
return res.data.map(m => m.split(";")?.[0])
} else return this.loader.status = "aborted" && Promise.reject()
}).finally(() => loading.close())
}
abort() {
}
}

View File

@@ -0,0 +1,34 @@
<template>
<div class="ai-empty">
<div class="ai-empty__bg"></div>
<template v-if="!isHasTitleSlot">暂无数据</template>
<slot v-else></slot>
</div>
</template>
<script>
export default {
name: 'AiEmpty',
computed: {
isHasTitleSlot () {
return this.$slots.default
}
}
}
</script>
<style lang="scss">
.ai-empty {
margin-bottom: 10px;
text-align: center;
color: #acaaad;
.ai-empty__bg {
background: url("https://cdn.cunwuyun.cn/dvcp/empty.svg") no-repeat center;
background-size: 120px 120px;
height: 120px;
margin: 48px auto 0;
}
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="ai-filelist">
<div class="ai-flie__item" v-for="(item, index) in fileList" :key="index" @click="downFile(item)"
:title="item.name">
<div class="ai-flie__item--left flex1">
<svg aria-hidden="true">
<use xlink:href="#iconAppendix_UNdownload"></use>
</svg>
<span>{{ item[fileProps.name] }}</span>
</div>
<div class="ai-file__item--right">
<span>{{ item[fileProps.size] }}</span>
<i class="iconfont iconDownload"></i>
</div>
</div>
<div v-if="!fileList.length" style="width: 120px" class="no-data"></div>
</div>
</template>
<script>
export default {
name: 'AiFileList',
props: {
fileList: {
type: Array,
default: () => []
},
fileOps: {
type: Object
}
},
computed: {
fileProps() {
const props = {
name: 'name',
size: 'size'
}
return this.fileOps || props
}
},
methods: {
downFile(item) {
window.open(`${item.url}`)
}
}
}
</script>
<style lang="scss" scoped>
.ai-filelist {
.ai-flie__item {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
line-height: 40px;
margin-bottom: 16px;
padding: 0 8px;
font-size: 14px;
color: #333;
background: #fff;
border-radius: 4px;
border: 1px solid #d0d4dc;
cursor: pointer;
&:hover {
background-color: #f3f6f9;
border-color: transparent;
}
&:last-child {
margin-bottom: 0;
}
}
.ai-flie__item--left {
display: flex;
align-items: center;
margin-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
svg {
width: 24px;
height: 24px;
flex-shrink: 1;
}
span {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.ai-file__item--right {
display: flex;
align-items: center;
flex-shrink: 1;
span {
padding-right: 5px;
color: #999;
}
i {
color: #5088FF;
}
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div class="AiIcon">
<div v-if="type=='icon'" class="iconfont" :class="icon"/>
<div v-if="type=='logo'" class="logofont" :class="icon"/>
<svg v-if="type=='svg'" class="icon" aria-hidden="true">
<use :xlink:href="`#${icon}`"></use>
</svg>
</div>
</template>
<script>
import "../../meta/styles/iconfont/iconfont";
import "../../meta/styles/iconfont/iconfont.css";
import "../../meta/styles/iconfont/logofont.css";
export default {
name: "AiIcon",
props: {
type: {type: String, default: "icon"},
icon: {type: String, required: true},
}
}
</script>
<style lang="scss" scoped>
.AiIcon {
box-sizing: border-box;
width: 28px;
height: 28px;
font-size: 16px;
.iconfont, .logofont, .icon {
font-size: inherit;
width: inherit;
height: inherit;
}
}
</style>

View File

@@ -0,0 +1,321 @@
<template>
<section class="ai-import">
<a v-if="$slots.default" class="custom-clicker" @click="dialog = true">
<slot/>
</a>
<el-button v-else size="small" @click="dialog=true" icon="iconfont iconImport">导入</el-button>
<ai-dialog
:title="dialogTitle"
:visible.sync="dialog"
:destroy-on-close="true"
width="800px"
customFooter
:close-on-click-modal="false"
@closed="onClose">
<el-form size="small" ref="importForm" label-width="0" class="import-form" :model="fileForm"
:rules="fileRules">
<el-form-item class="ai-import__tips">
<h2>导入说明</h2>
<p class="ai-import__content">
1您正在进行{{ name }}批量导入操作请先
<span @click="downloadFile" title="下载导入模板" class="ai-link" v-text="'【点击下载导入模板】'"/>
并规范填写</p>
<p class="ai-import__content">2填写完成后上传您编辑完成的模板文件</p>
<div class="ai-import__text">
<template v-if="$slots.tips">
<div>请注意</div>
<slot name="tips"/>
</template>
<el-row type="flex" v-else-if="!!dict">
<div v-text="'请注意:'"/>
<span v-text="dict.getLabel('importTips',type)"/>
</el-row>
</div>
</el-form-item>
<el-form-item prop="file" style="width: 100%">
<ai-uploader isImport :instance="instance" v-model="fileForm.file" fileType="file" :limit="1"
acceptType=".xls,.xlsx" @change="onChange" :clearable="false">
<template #trigger>
<el-button icon="iconfont iconfangda">选择文件</el-button>
</template>
<template #tips>最多上传1个文件,单个文件最大10MB仅支持Excel格式</template>
</ai-uploader>
</el-form-item>
</el-form>
<div slot="footer" style="text-align: center;">
<el-button @click="dialog=false">取消</el-button>
<el-button type="primary" @click="onClick">立即导入</el-button>
</div>
</ai-dialog>
<div class="ai-import__loading" v-if="isHasLoadingSlot && isLoading">
<slot name="loading"/>
</div>
</section>
</template>
<script>
export default {
name: 'AiImport',
props: {
title: String,
name: {
type: String,
required: true
},
instance: {
type: Function
},
importUrl: {
type: String,
},
importParams: {
type: Object
},
suffixName: {
type: String,
default: 'xls'
},
tplParams: Object,
url: {
type: String,
},
timeout: {
type: Number,
default: 10 * 60 * 1000
},
customError: {
type: Boolean,
default: false
},
type: String,
dict: Object
},
computed: {
isHasLoadingSlot() {
return this.$slots.loading
},
actions() {
return {
url: this.importUrl || `/app/${this.type}/import`,//导入接口
tpl: this.url || `/app/${this.type}/downloadTemplate`,//下载模板接口
}
},
dialogTitle() {
return this.title || `${this.name}数据导入`
}
},
data() {
return {
dialog: false,
fileForm: {
file: []
},
loading: null,
isLoading: false,
fileRules: {
file: [{required: true, message: '请上传相关文件', trigger: 'change'}]
}
}
},
methods: {
onChange(e) {
if (e.length) {
this.$refs.importForm.clearValidate()
} else {
this.$refs.importForm.validate()
}
},
onClick() {
this.$refs.importForm.validate(v => {
if (v) {
const data = new FormData()
data.append('file', this.fileForm.file[0].raw)
if (!this.isHasLoadingSlot) {
this.loading = this.$loading({
lock: true,
text: '导入中',
background: 'rgba(0, 0, 0, 0.5)'
})
} else {
this.isLoading = true
}
this.instance.post(this.actions.url, data, {
params: this.importParams,
timeout: this.timeout
}).then(res => {
if (!this.isHasLoadingSlot) {
this.loading?.close()
} else {
this.isLoading = false
}
if (res?.data?.importStatus == 1) {
this.dialog = false
const h = this.$createElement
this.$emit('onSuccess', res)
this.$emit('success', res)
if (this.customError) {
this.$emit('error')
} else this.$confirm('导入失败数据具体原因请', {
type: 'success',
title: '数据导入完成',
closeOnClickModal: false,
customClass: 'message-wrapper',
showConfirmButton: false,
cancelButtonText: '关闭',
message: h('div', {
class: 'importResult'
}, [
h('span', null, '成功新增'),
h('a', {
style: 'color: #2EA222;'
}, `${res.data.addCount}`),
h('span', null, '条数据,更新'),
h('a', {
style: 'color: #26f;'
}, `${res.data.updateCount}`),
h('span', null, '条数据,导入失败'),
h('a', {
style: 'color: #f46;'
}, `${res.data.failCount}`),
h('span', null, '条数据。'),
h('div', {class: 'gap'}),
h('div', {style: `display:${res.data.errorFileURL ? 'block' : 'none'}`}, [
h('span', null, '点此'),
h('a', {
class: 'tips-link',
attrs: {
href: res.data.errorFileURL
}
}, '下载异常数据')
])
])
})
} else if (res?.data?.importStatus == 0) {
this.$message.error(res?.data?.errorMsg)
}
}).catch(() => {
if (!this.isHasLoadingSlot) {
this.loading?.close()
} else {
this.isLoading = false
}
})
}
})
},
onClose() {
this.loading?.close()
this.fileForm.file = []
},
downloadFile() {
this.instance.post(this.actions.tpl, null, {
responseType: 'blob',
params: this.tplParams
}).then((res) => {
const link = document.createElement('a')
let blob = new Blob([res], {type: 'application/vnd.ms-excel'})
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.setAttribute('download', this.name + '模板.' + this.suffixName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
},
hide() {
this.dialog = false
}
},
created() {
this.dict?.load("importTips")
}
}
</script>
<style lang="scss" scoped>
.ai-import {
.empty-input {
opacity: 0;
position: absolute;
z-index: -1;
visibility: hidden;
}
.custom-clicker {
display: flex;
align-items: center;
}
.ai-import__loading {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
}
::v-deep .el-message-box {
width: 720px !important;
}
.ai-import__content {
line-height: 22px;
}
.ai-import__tips {
line-height: 1;
::v-deep.el-form-item__content {
line-height: 1;
}
h2 {
margin-top: 4px;
color: #333333;
font-size: 16px;
font-weight: 700;
}
p {
margin-top: 8px;
color: #424242;
font-size: 14px;
}
.ai-link {
cursor: pointer;
color: $primaryColor;
}
.ai-import__text {
font-size: 12px;
margin-top: 8px;
line-height: 16px;
color: #999999;
}
}
}
</style>
<style lang="scss">
.message-wrapper {
width: 560px !important;
.importResult {
color: #222222;
font-size: 16px;
line-height: 24px;
font-weight: bold;
.gap {
width: 100%;
height: 8px;
}
.tips-link {
color: $primaryColor;
text-decoration: unset;
}
}
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<div class="ai-info-item" :style="contentStyle">
<label class="ai-info-item__left" :style="contentLabelStyle">
<template v-if="$slots.label">
<slot name="label"/>
</template>
<template v-else>{{ label }}</template>
</label>
<div class="ai-info-item__right">
<slot v-if="$scopedSlots.default"/>
<template v-else-if="!!openType">
<ai-open-data :type="openType" :openid="value"/>
</template>
<template v-else>{{ value || '-' }}</template>
</div>
</div>
</template>
<script>
export default {
name: 'AiInfoItem',
inject: ['AiWrapper'],
props: {
label: {
type: String
},
value: {
type: [String, Number]
},
'label-width': {
type: String
},
isLine: {
type: Boolean,
default: false
},
openType: {default: ""}
},
computed: {
contentStyle() {
let width = this.AiWrapper.autoWidth
if (this.isLine) {
width = '100%'
}
return {
width
}
},
contentLabelStyle() {
return {
width: this.labelWidth || this.AiWrapper.autoLableWidth
}
},
},
methods: {}
}
</script>
<style lang="scss" scoped>
.ai-info-item {
display: flex;
line-height: 1.4;
margin-bottom: 16px;
label {
flex-shrink: 0;
width: 96px;
margin-right: 40px;
text-align: right;
color: #888;
font-size: 14px;
}
.ai-info-item__right {
flex: 1;
color: #222;
font-size: 14px;
word-break: break-all;
}
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<section class="AiNumber">
<el-input v-model="num" :size="size" @input.native="validate" @focus="$emit('focus')" @blur="format" clearable
@change="$emit('change')" :validate-event="isRule"></el-input>
</section>
</template>
<script>
export default {
name: "AiNumber",
model: {
prop: "value",
event: "change"
},
props: {
value: String,
size: String,
decimal: {type: [String, Number], default: 2},
isRule: {type: Boolean, default: true}
},
computed: {
validRegex() {
let dec = Number(this.decimal || 0),
regex = `^(\\d*\\.?\\d{0,${dec}}).*`
return new RegExp(regex)
}
},
data() {
return {
num: ""
}
},
methods: {
validate() {
let num = JSON.parse(JSON.stringify(this.num))
this.num = num.replace(this.validRegex, '$1')
},
format() {
this.num = Number(this.num||0).toString()
this.$emit("blur")
}
},
watch: {
num(v) {
this.$emit("change", v)
}
},
mounted() {
this.num = this.value
}
}
</script>
<style lang="scss" scoped>
.AiNumber {
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<div class="ai-operate">
<template v-if="filterBtns.length <= 3">
<el-button
v-for="(item, index) in filterBtns"
:key="index"
type="text"
:title="item.text"
@click="onClick(index)">
{{ item.text }}
</el-button>
</template>
<template v-else>
<el-button
v-for="(item, index) in list"
:key="index"
type="text"
:title="item.text"
@click="onClick(index)">
{{ item.text }}
</el-button>
<el-dropdown
trigger="click"
@command="onDropClick">
<el-button
style="margin-left: 10px;"
title="更多"
type="text">
更多
</el-button>
<el-dropdown-menu
slot="dropdown">
<el-dropdown-item
v-for="(item, index) in moreList"
:key="index"
:command="index">
{{ item.text }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</div>
</template>
<script>
export default {
name: 'AiOperate',
props: {
permissions: {
type: Function,
required: true
},
btns: {
type: Array,
required: true,
default: () => []
}
},
computed: {
filterBtns() {
return this.btns.filter(e => this.permissions(e.permissions))
}
},
data() {
return {
list: [],
moreList: []
}
},
mounted() {
this.filterBtns.forEach((item, index) => {
if (index <= 1) {
this.list.push(item)
} else {
this.moreList.push(item)
}
})
},
methods: {
onClick(index) {
this.$emit('on-click', index)
},
onDropClick(e) {
this.onClick(e + 2)
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,76 @@
<template>
<section class="AiPullDown">
<div class="line"/>
<div class="down-content" @click="handleExpand">
<i :class="expandIcon"/>
<span>{{ btnText }}</span>
</div>
<div class="line"/>
</section>
</template>
<script>
export default {
name: "AiPullDown",
props: {
target: String,
height: {default: 4},
},
data() {
return {
expand: false
}
},
methods: {
handleExpand() {
this.expand = !this.expand
if (this.target) {
} else this.$emit('change', this.expandStyle)
}
},
computed: {
btnText() {
return this.expand ? '收起高级搜索' : '展开高级搜索'
},
expandStyle() {
let initStyle = {overflow: 'hidden', height: `${this.height}px`}
this.expand && (initStyle.height = "auto")
return initStyle
},
expandIcon() {
return this.expand ? 'iconfont iconDouble_Up' : 'iconfont iconDouble_Down'
}
},
mounted() {
this.$emit("change", this.expandStyle)
}
}
</script>
<style lang="scss" scoped>
.AiPullDown {
display: flex;
.line {
flex: 1;
min-width: 0;
border-top: 1px solid #eee;
}
.down-content {
cursor: pointer;
padding: 0 8px;
height: 24px;
border-radius: 0 0 8px 8px;
border: 1px solid #eee;
border-top: 0;
box-sizing: border-box;
color: #333;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<section class="AiRange" :style="{width}">
<el-input type="number" v-model.number="valueStart" size="small" :placeholder="startPlaceholder" @change="saveNum"/>
<span class="separator">-</span>
<el-input type="number" v-model.number="valueEnd" size="small" :placeholder="endPlaceholder" @change="saveNum"/>
<span class="el-icon-circle-close" v-if="isUse" @click="shutoff"/>
<div v-else/>
</section>
</template>
<script>
export default {
name: "AiRange",
model: {
prop: 'value',
event: 'change'
},
props: {
width: {type: String, default: '200px'},
startPlaceholder: String,
endPlaceholder: String,
value: {default: () => []},
},
computed: {
isUse() {
return !!this.valueStart || !!this.valueEnd
},
},
data() {
return {
valueStart: null,
valueEnd: null
}
},
methods: {
shutoff() {
this.valueStart = null
this.valueEnd = null
this.saveNum()
},
saveNum() {
this.$emit('change', [this.valueStart, this.valueEnd])
},
initValue() {
let unwatch = this.$watch('value', (v) => {
if (this.isUse) unwatch && unwatch()
else if (!!v) {
this.valueStart = v?.[0] || ""
this.valueEnd = v?.[1] || ""
unwatch && unwatch()
}
}, {immediate: true})
},
},
created() {
this.initValue()
}
}
</script>
<style scoped lang="scss">
.AiRange {
display: flex;
border: 1px solid #D0D4DC;
border-radius: 2px;
align-items: center;
height: 32px;
box-sizing: border-box;
::v-deep.el-input {
min-width: 80px;
flex: 1;
.el-input__inner {
border: none;
padding: 0;
text-align: center;
height: 100%;
line-height: normal;
}
}
.el-icon-circle-close {
cursor: pointer;
opacity: 0;
margin-right: 4px;
&:hover {
color: #5088FF;
}
}
&:hover {
border-color: #5088FF;
.el-icon-circle-close {
opacity: 1;
}
}
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<div class="ai-select">
<el-select
style="width: 100%;"
clearable
:value="value"
:size="$attrs.size || 'small'"
:filterable="isAction"
v-bind="$attrs"
v-on="$listeners">
<template v-if="isAction">
<el-option v-for="op in actionOps" :key="op.id"
:label="op[actionProp.label]" :value="op[actionProp.value]"/>
</template>
<template v-else>
<el-option
v-for="(item, index) in selectList"
:key="index"
:label="item.dictName"
:value="item.dictValue"/>
</template>
</el-select>
</div>
</template>
<script>
export default {
name: 'AiSelect',
model: {
prop: 'value',
event: 'change'
},
watch: {
instance: {
deep: true,
handler(v) {
v && this.isAction && !this.options.toString() && this.getOptions()
}
}
},
props: {
value: {default: null},
selectList: {
type: Array
},
width: {
type: String,
default: '216'
},
instance: Function,
action: {default: ""},
prop: {
default: () => ({})
}
},
data() {
return {
options: [],
filter: ""
}
},
computed: {
selectWidth() {
if (this.width.indexOf('px') > -1) {
return this.width
}
return `${this.width}px`
},
isAction() {
return !!this.action
},
actionOps() {
return this.options.filter(e => !this.filter || e[this.actionProp.label].indexOf(this.filter) > -1)
},
actionProp() {
return {
label: 'label',
value: 'id',
...this.prop
}
}
},
methods: {
getOptions() {
this.instance?.post(this.action, null, {
params: {size: 999}
}).then(res => {
if (res?.data) {
this.options = res.data.records || res.data
}
})
}
},
created() {
this.getOptions()
}
}
</script>
<style lang="scss" scoped>
::v-deep .ai-select .el-select {
width: 100%;
}
</style>

View File

@@ -0,0 +1,446 @@
<template>
<div class="ai-table" :class="[isShowBorder ? 'ai-table__border' : 'ai-table__noborder']">
<el-table
:data="tableData"
header-cell-class-name="ai-table__header"
cell-class-name="ai-table__cell"
row-class-name="ai-table__row"
:class="{'ai-header__border': isShowBorder}"
:ref="refName"
:size="tableSize"
:stripe="stripe"
:tooltip-effect="tooltipEffect"
@selection-change="handleSelectionChange"
v-on="$listeners"
v-bind="$attrs"
v-loading="loading">
<template v-for="colConfig in colConfigs.filter(e=>!e.hide)">
<slot v-if="colConfig.slot && colConfig.slot !== 'options'" :name="colConfig.slot"/>
<component
:key="colConfig.id"
v-else-if="colConfig.component"
:is="colConfig.component"
:col-config="colConfig">
</component>
<el-table-column
v-else-if="colConfig.dict"
:key="colConfig.id"
v-bind="colConfig">
<span slot-scope="{row}" :style="{color:colConfig.color||dict.getColor(colConfig.dict, row[colConfig.prop])}">
{{ dict.getLabel(colConfig.dict, row[colConfig.prop]) }}
</span>
</el-table-column>
<el-table-column
v-else-if="colConfig.openType"
:key="colConfig.id"
v-bind="colConfig">
<template v-slot="{row}">
<ai-open-data :type="colConfig.openType" :openid="row[colConfig.prop]"/>
</template>
</el-table-column>
<el-table-column
v-else-if="colConfig.type"
:key="colConfig.id"
v-bind="colConfig"
:width="colConfig.width || 100"/>
<el-table-column v-else v-bind="colConfig" :key="colConfig.id"
:show-overflow-tooltip="colConfig['show-overflow-tooltip'] != false">
<template slot-scope="scope">
<render-slot v-if="colConfig.render" :render="colConfig.render" :row="scope.row" :index="scope.$index"
:column="colConfig"/>
<span v-else>{{ getValue(colConfig, scope.row) }}</span>
</template>
</el-table-column>
</template>
<slot class="table-options" name="options"></slot>
<template #empty>
<slot v-if="$scopedSlots.empty" name="empty"/>
<div v-else class="no-data" style="height:160px;"/>
</template>
</el-table>
<div class="pagination newPagination" v-if="isShowPagination">
<el-pagination
background
:current-page.sync="page.current"
:total="page.total"
:page-size="page.size"
v-bind="$attrs"
:page-sizes="pageSizes"
:layout="layout"
:pager-count="page.pagerCount"
@size-change="handleSizeChange"
@current-change="handleChange">
<div class="paginationPre">
<el-checkbox v-if="isHasPaginationBtnsSlot" :disabled="!tableData.length" :indeterminate="isIndeterminate"
:value="checkAll"
@click.native="toggleAllSelection">全选
</el-checkbox>
<slot name="pagination"/>
<div class="pagination-btns">
<slot name="paginationBtns"></slot>
</div>
<div class="paginationPre-total" :style="{marginLeft: isHasPaginationBtnsSlot ? '24px' : 0}"><label class="color-primary">{{ page.total }}</label>条记录
</div>
</div>
</el-pagination>
</div>
</div>
</template>
<script>
import moment from 'dayjs'
import dict from "../../meta/js/dict"
let renderSlot = {
functional: true,
props: {
row: Object,
render: Function,
index: Number,
column: {type: Object, default: null},
},
render: (h, data) => {
const params = {
row: data.props.row,
index: data.props.index
}
if (data.props.column) {
params.column = data.props.column
}
return data.props.render(h, params)
}
}
export default {
name: 'AiTable',
props: {
colConfigs: Array,
tableData: Array,
current: {
type: Number,
default: 1
},
size: {
type: Number,
default: 10
},
isShowPagination: {
type: Boolean,
default: true
},
total: {
type: Number
},
layout: {
type: String,
default: 'slot,->, prev, pager, next, sizes, jumper'
},
stripe: {
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
},
tooltipEffect: {
type: String
},
tableSize: {
type: String
},
tableRef: String,
dict: {default: () => dict},
pagerCount: {default: 5},
pageSizes: {default: () => [10, 20, 50, 100]}
},
data() {
return {
name: '',
chooseList: []
}
},
components: {renderSlot},
computed: {
refName() {
return this.tableRef || `aiTable${new Date().getTime()}`
},
isShowBorder() {
return !!this.$attrs.border || this.$attrs.border === ''
},
isHasPaginationBtnsSlot() {
return this.$slots.paginationBtns
},
page() {
return {
current: this.current,
size: this.size,
total: this.total,
pagerCount: this.pagerCount
}
},
isIndeterminate() {
return this.chooseList.length > 0 && this.chooseList.length < this.tableData.length
},
checkAll() {
return this.chooseList.length == this.tableData.length && this.tableData !== 0
}
},
methods: {
handleChange(e) {
this.$emit('update:current', e)
this.$nextTick(() => {
this.$emit('getList')
})
},
handleSizeChange(e) {
this.$emit('update:size', e)
this.$nextTick(() => {
this.$emit('getList')
})
},
handleSelectionChange(e) {
this.chooseList = e
this.$emit('handleSelectionChange', e)
},
getValue(colConfig, row) {
if (this.isFunction(colConfig.formart)) {
return colConfig.formart.call(this, row[colConfig.prop])
}
if (colConfig.dateFormat) {
return moment(row[colConfig.prop]).format(colConfig.dateFormat)
}
return this.isInvalidValue(row[colConfig.prop])
},
isInvalidValue(value) {
if (value === null || value === undefined || value === '') {
return '-'
}
return value
},
isFunction(fun) {
return typeof fun === 'function'
},
/**
* 表格方法代理
*/
clearSelection() {
this.$refs[this.refName].clearSelection()
},
toggleRowSelection() {
this.$refs[this.refName].toggleRowSelection(...arguments)
},
toggleAllSelection() {
this.$refs[this.refName].toggleAllSelection()
},
toggleRowExpansion() {
this.$refs[this.refName].toggleRowExpansion(...arguments)
},
setCurrentRow() {
this.$refs[this.refName].setCurrentRow(...arguments)
},
clearSort() {
this.$refs[this.refName].clearSort()
},
clearFilter() {
this.$refs[this.refName].clearFilter(...arguments)
},
doLayout() {
this.$refs[this.refName].doLayout()
},
sort() {
this.$refs[this.refName].sort(...arguments)
},
}
}
</script>
<style lang="scss" scoped>
.ai-table {
.color-primary {
color: $primaryColor;
}
::v-deep .ai-header__border .ai-table__header {
border-bottom: 1px solid $borderColor !important;
border-right: 1px solid $borderColor !important;
}
::v-deep .el-table--border {
border: 1px solid $borderColor;
border-right: none;
border-bottom: none;
}
::v-deep .el-table {
color: #222;
.caret-wrapper {
height: 24px;
.ascending {
top: 1px;
}
.descending {
bottom: 1px;
}
}
thead {
color: #555
}
}
::v-deep .cell {
line-height: 24px;
}
::v-deep .el-table__header {
th {
padding: 8px 0;
}
tr {
.cell {
font-weight: 700;
}
th:first-child {
.cell {
padding-left: 40px !important;
}
}
}
}
::v-deep .el-table__body {
tr td:first-child .cell {
padding-left: 40px !important;
}
}
::v-deep .el-table__fixed-right {
.el-table__body {
tr td:first-child .cell {
padding-left: 0 !important;
padding-right: 0;
}
}
}
::v-deep .ai-table__header {
border-bottom: none;
background: #F3F4F5;
}
::v-deep.el-pager {
li.active + li {
border-left: 1px solid $borderColor;
}
}
.newPagination {
width: 100%;
display: flex;
align-items: center;
height: 64px;
padding: 0 40px !important;
.el-pagination {
width: 100%;
padding: 0;
}
.paginationPre {
display: flex;
height: 28px;
line-height: 1;
font-size: 14px;
font-weight: normal;
align-items: center;
.pagination-btns {
display: flex;
align-items: center;
gap: 8px;
color: $primaryColor !important;
::v-deep span, ::v-deep div {
font-size: 12px;
cursor: pointer;
color: $primaryColor !important;
&:hover {
opacity: 0.8;
}
}
}
.paginationPre-total {
font-size: 12px;
color: #555;
label {
padding: 0 2px;
font-weight: 700;
}
}
& > * + * {
margin-left: 24px;
}
::v-deep .el-pagination button, .el-pagination span:not([class*=suffix]) {
line-height: 1 !important;
}
::v-deep.el-checkbox {
display: flex;
align-items: center;
.el-checkbox__input, .el-checkbox__inner {
width: 14px;
height: 14px;
min-width: 0 !important;
line-height: 1 !important;
}
.el-checkbox__label {
font-size: 12px;
color: #222222;
height: auto !important;
line-height: 1 !important;
padding-left: 3px !important;
}
}
}
}
}
.ai-table__noborder {
::v-deep .el-table td, ::v-deep .el-table th.is-center {
border: none;
}
.el-table::before {
display: none;
}
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
background: #F5F6F9;
}
::v-deep .el-table__fixed-right::before, ::v-deep .el-table__fixed::before {
display: none;
}
}
</style>

View File

@@ -0,0 +1,242 @@
<template>
<section class="AiTableSelect">
<el-row type="flex">
<ai-table v-if="isShowPagination" ref="PendingTable" :tableData="tableData" :total="page.total" :current.sync="page.current"
:size.sync="page.size" class="fill" border height="330px" @getList="getTableData" tableSize="mini"
:col-configs="[{slot: 'resident'}]" layout="slot,->, prev, pager, next, jumper" :pagerCount="5">
<el-table-column slot="resident">
<template #header>
<b v-text="tableTitle"/>
<el-input class="fill searchbar" v-model="search[searchKey]" size="small" placeholder="搜索" clearable
@change="page.current=1,getTableData()"/>
</template>
<template slot-scope="{row}">
<slot name="pending" v-if="$scopedSlots.pending" :row="row"/>
<el-row v-else type="flex" justify="space-between" @click.native="handleSelect(row)" class="toggle"
:class="{selected:findSelected(row)>-1}">
<span v-text="row[nodeName]"/>
<slot name="extra" v-if="$scopedSlots.extra" :row="row"/>
<span v-else v-text="getExtra(row)"/>
</el-row>
</template>
</el-table-column>
</ai-table>
<ai-table v-else ref="PendingTable" :tableData="tableData" class="fill" border height="330px" @getList="getTableData" tableSize="mini"
:col-configs="[{slot: 'resident'}]" :isShowPagination="false">
<el-table-column slot="resident">
<template #header>
<b v-text="tableTitle"/>
<el-input class="fill searchbar" v-model="search[searchKey]" size="small" placeholder="搜索" clearable
@change="page.current=1,getTableData()"/>
</template>
<template slot-scope="{row}">
<slot name="pending" v-if="$scopedSlots.pending" :row="row"/>
<el-row v-else type="flex" justify="space-between" @click.native="handleSelect(row)" class="toggle"
:class="{selected:findSelected(row)>-1}">
<span v-text="row[nodeName]"/>
<slot name="extra" v-if="$scopedSlots.extra" :row="row"/>
<span v-else v-text="getExtra(row)"/>
</el-row>
</template>
</el-table-column>
</ai-table>
<ai-table :tableData="selected" :col-configs="[{slot:'resident'}]" :isShowPagination="false" border
height="330px" tableSize="mini" class="el-table--scrollable-y">
<el-table-column slot="resident">
<template #header>
<b v-text="`已选择`"/>
<el-button type="text" @click="selected=[]">清空</el-button>
</template>
<template slot-scope="{row,$index}">
<slot name="selected" v-if="$scopedSlots.selected" :row="row" :index="$index"/>
<el-row v-else type="flex" align="middle" justify="space-between">
<div v-text="[row[nodeName], getExtra(row)].join(' ')"/>
<el-button type="text" @click="selected.splice($index,1)">删除</el-button>
</el-row>
</template>
</el-table-column>
</ai-table>
</el-row>
</section>
</template>
<script>
/**
* 智能列表选择器
* @displayName AiTableSelect
*/
export default {
name: "AiTableSelect",
model: {
event: "change",
prop: "value"
},
props: {
/**
* 接口方法类:必填
*/
instance: {type: Function, required: true},
/**
* 接口方法地址:必填
*/
action: {default: "", required: true},
/**
* 选择表格标题
*/
tableTitle: {default: "选择列表"},
/**
* 选择项
* @model
*/
value: {default: ""},
/**
* 选择项绑定key
*/
nodeKey: {default: "id"},
/**
* 选择项绑定展示值:必填
*/
nodeName: {default: "name"},
/**
* 是否多选
*/
multiple: Boolean,
/**
* 返回值为选择的对象而不是id
*/
valueObj: Boolean,
/**
* 扩展字段,用于右侧展示
*/
extra: {default: ""},
/**
* 搜索字段,用于搜索框
*/
searchKey: {default: "con"},
/**
* 是否分页
*/
isShowPagination: {default: true}
},
data() {
return {
page: {total: 0, current: 1, size: 10},
search: {},
tableData: [],
selected: [],
}
},
watch: {
action(v) {
v && (this.page.current = 1, this.getTableData())
},
selected: {
deep: true, handler(v) {
let ids = v.map(e => e[this.nodeKey] || e).filter(e => !!e)
this.$emit("change", this.valueObj ? v : this.multiple ? ids : ids?.toString())
this.$emit("select", v)
}
}
},
methods: {
getExtra(row) {
let {extra} = this
return extra ? row[extra] : this.idCardNoUtil.hideId(row.idNumber)
},
initValue() {
let unwatch = this.$watch('value', (v) => {
if (this.selected.length > 0) unwatch && unwatch()
else if (!!v) {
this.selected = this.multiple ? [v].flat().filter(e => !!e) || [] : (v?.split(",") || [])
unwatch && unwatch()
}
}, {immediate: true})
},
getTableData() {
let {page, search, action} = this
this.instance?.post(action, null, {
params: {...page, ...search}
}).then(res => {
if (res?.data) {
this.tableData = res.data.records || res.data || []
this.isShowPagination && (this.page.total = res.data.total)
}
})
},
handleSelect(row) {
let index = this.findSelected(row)
if (index > -1) {
this.selected.splice(index, 1)
} else {
if (this.multiple) {
this.selected.push(row)
} else {
this.selected = [row]
}
}
this.$forceUpdate()
},
findSelected(item) {
let {nodeKey} = this
return this.selected?.findIndex(e => e[nodeKey] == item[nodeKey])
},
},
created() {
this.$set(this.search, this.searchKey, "")
this.initValue()
this.getTableData()
}
}
</script>
<style lang="scss" scoped>
::v-deep.AiTableSelect {
.el-row {
width: 100%;
.ai-table + .ai-table {
margin-left: 16px;
width: 400px;
.ai-table__header > .cell {
line-height: 40px;
}
}
}
.toggle {
cursor: pointer;
&.selected {
color: $primaryColor;
}
}
.ai-table__header {
padding: 0 !important;
& > .cell {
display: flex;
align-items: center;
justify-content: space-between;
}
}
.searchbar {
padding-right: 0;
}
.newPagination {
height: 32px;
padding: 0 !important;
margin-top: 8px;
}
.ai-table__cell {
.el-button--text {
padding: 0 8px;
height: 24px;
}
}
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<section class="AiTitle" :class="{ 'bottomBorder': isShowBottomBorder, AiTitleSub: isHasSub}">
<i class="iconfont iconBack_Large" v-if="isShowBack" @click="onBackBtnClick"/>
<div class="fill">
<div class="ailist-title">
<div class="ailist-title__left">
<h2>{{ title }}</h2>
<div v-if="isShowIM" class="openIM iconfont iconGroup_IM" @click="openIM"></div>
</div>
<div class="ailist-title__right">
<ai-area
v-if="isShowArea"
:instance="instance"
v-bind="$attrs"
:value="value"
v-on="$listeners"
:areaLevel="areaLevel"
:hideLevel="hideLevel"
:valueLevel="valueLevel"
:disabled="disabled"/>
<div class="aititle-right__btns">
<slot name="rightBtn"></slot>
</div>
</div>
</div>
<div class="subtitle" v-if="$scopedSlots.sub">
<slot name="sub"/>
</div>
</div>
</section>
</template>
<script>
export default {
name: 'AiTitle',
model: {
prop: 'value',
event: 'change'
},
props: {
title: {
type: String,
required: true
},
instance: {
type: Function
},
areaLevel: [String, Number],
hideLevel: {default: 2},
valueLevel: [String, Number],
disabled: {
type: Boolean,
default: false
},
value: {
type: String
},
isShowIM: {
type: Boolean,
default: false
},
isShowArea: {
type: Boolean,
default: false
},
openIM: {
type: Function
},
isShowBack: {
type: Boolean,
default: false
},
isShowBottomBorder: {
type: Boolean,
default: false
},
classic: Boolean
},
computed: {
isHasSub() {
return this.$slots.sub
}
},
methods: {
onBackBtnClick() {
this.$emit('onBackClick')
this.$emit('back')
}
}
}
</script>
<style scoped lang="scss">
.AiTitle {
display: flex;
.ailist-title {
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
.ailist-title__left {
display: flex;
align-items: center;
& > i {
width: auto;
height: auto;
margin-right: 8px;
font-size: 16px;
}
h2 {
color: #222;
font-size: 16px;
font-weight: 600;
}
.openIM {
margin-left: 8px;
}
}
.ailist-title__right {
display: flex;
align-items: center;
}
::v-deep.el-button {
margin-left: 8px !important;
}
}
&.AiTitleSub {
height: auto;
padding: 16px 0;
.ailist-title {
height: auto;
margin-bottom: 3px;
}
}
&.bottomBorder {
border-bottom: 1px solid #D8DCE3;
}
.subtitle {
width: 100%;
color: #888888;
font-size: 12px;
margin-top: 4px;
}
.iconBack_Large {
line-height: 48px;
margin-right: 8px;
}
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<el-row type="flex" align="middle">
<el-button type="text" size="small" :loading="loading" icon="iconfont iconClock" title="语音播报"
@click="getSpeechByContent"/>
<ai-audio v-if="speech" :src="speech" skin="flat"/>
</el-row>
</template>
<script>
export default {
name: "AiTransSpeech",
props: {
instance: {type: Function, required: true},
content: String
},
data() {
return {
speech: "",
loading: false
}
},
methods: {
getSpeechByContent() {
this.loading = true
this.instance.post("/app/msc/transToSpeech", null, {
params: {
fileName: "demo",
words: this.content
}
}).then(res => {
this.loading = false
if (res && res.data) {
let url = res.data.join("")
this.speech = url.substring(0, url.indexOf(";"))
}
}).catch(() => this.loading = false)
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,514 @@
<template>
<section class="uploader">
<el-upload
action
multiple
ref="upload"
:class="{validError:!validateState}"
:http-request="submitUpload"
:on-remove="handleRemove"
:on-change="handleChange"
:before-upload="onBeforeUpload"
:file-list="fileList"
:limit="limit"
:disabled="disabled"
:list-type="isImg ? 'picture-card' : 'text'"
:accept="accept"
:show-file-list="!isSingle"
:on-preview="handlePictureCardPreview"
:auto-upload="isAutoUpload"
:on-exceed="handleExceed">
<template v-if="!disabled">
<template v-if="hasUploaded&&isSingle">
<div class="fileItem">
<div class="uploadFile" @click.stop>
<ai-icon type="svg" :icon="uploadFile.icon"/>
<div class="info">
<span v-text="uploadFile.name"/>
<span class="size" v-text="uploadFile.size"/>
</div>
</div>
<el-button>重新选择</el-button>
<el-button v-if="clearable" plain type="danger" @click.stop="handleClear">删除</el-button>
</div>
</template>
<template v-else-if="limit > fileList.length">
<slot v-if="hasTriggerSlot" name="trigger"/>
<div v-else class="uploaderBox">
<span class="iconfont" :class="isImg ? 'iconPhoto' : 'iconAdd'"/>
<p>上传{{ isImg ? '图片' : '附件' }}</p>
</div>
</template>
<div slot="tip" class="el-upload__tip" v-if="showTips">
<p v-if="fileType === 'img' && !acceptType && !hasTipsSlot">最多上传{{
limit
}}张图片,单个文件最大10MB支持jpgjpegpng格式</p>
<p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">最多上传{{ limit }}个附件,单个文件最大10MB</p>
<p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">
支持.zip.rar.doc.docx.xls.xlsx.ppt.pptx.pdf.txt.jpg.png格式</p>
<p>
<slot name="tips" v-if="hasTipsSlot"></slot>
</p>
</div>
</template>
</el-upload>
<el-dialog :visible.sync="dialog" title="图片预览编辑器" :modal="false" :show-close="false" append-to-body>
<vue-cropper
ref="cropper"
style="height: 400px;"
:img="fileList.length ? fileList[0].url : ''" v-bind="crop"/>
<div style="text-align: center;margin-top: 10px;">
<el-radio-group v-if="crop.fixed" size="small" v-model="currFixedIndex" @change="onFixedChange"
style="margin-right: 8px;">
<el-radio-button
:label="0"
size="small">
1.6:1
</el-radio-button>
<el-radio-button
:label="1"
size="small">
4:3
</el-radio-button>
</el-radio-group>
<el-button size="small" circle icon="el-icon-refresh-right" @click="$refs.cropper.rotateRight()"></el-button>
<el-button size="small" circle icon="el-icon-refresh-left" @click="$refs.cropper.rotateLeft()"></el-button>
</div>
<div slot="footer">
<el-popconfirm title="是否关闭图片预览器,并上传图片?" @confirm="previewCrop">
<el-button slot="reference" type="primary">保存</el-button>
</el-popconfirm>
<el-button @click="onClose">关闭</el-button>
</div>
</el-dialog>
<div class="images" v-viewer="{movable: true}" v-show="false">
<img v-for="(item, index) in imgList" :src="item" :key="index" alt="">
</div>
</section>
</template>
<script>
import {VueCropper} from 'vue-cropper'
import 'viewerjs/dist/viewer.css'
import Viewer from 'v-viewer'
import Vue from "vue";
Viewer.setDefaults({
zIndex: 20170
})
Vue.use(Viewer)
export default {
name: 'AiUploader',
components: {VueCropper},
inject: {
elFormItem: {default: ""},
elForm: {default: ''},
},
model: {
prop: 'value',
event: 'change'
},
props: {
value: {default: () => []},
url: {
type: String,
default: '/admin/file/add'
},
isShowTip: {
type: Boolean,
default: false
},
isWechat: {
type: Boolean,
default: false
},
maxSize: {
type: Number,
default: 10
},
instance: Function,
acceptType: {type: String},
fileType: {type: String, default: 'img'},
limit: {type: Number, default: 9},
disabled: {type: Boolean, default: false},
isCrop: {type: Boolean, default: false},
cropOps: Object,
isImport: {
type: Boolean,
default: false
},
clearable: {default: true},
valueIsUrl: Boolean
},
data() {
return {
fileList: [],
dialog: false,
currFixedIndex: 0,
}
},
watch: {
value: {
handler(v) {
this.dispatch('ElFormItem', 'el.form.change', [v]);
if (v?.length > 0) {
this.fileList = this.valueIsUrl ? v?.split(",")?.map(url => ({url})) : [...v]
}
},
immediate: true,
deep: true
}
},
computed: {
isImg() {
return this.fileType === 'img'
},
validateState() {
return ['', 'success'].includes(this.elFormItem?.validateState)
},
isAutoUpload() {
return !(this.isCrop || this.isImport);
},
hasTipsSlot() {
return this.$slots.tips
},
hasTriggerSlot() {
return this.$slots.trigger
},
accept() {
if (this.acceptType) {
return this.acceptType
}
return this.isImg ? '.jpg,.png,.jpeg' : '.zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt,.jpg,.png,.mp4'
},
crop() {
return {
autoCrop: true,
outputType: 'png',
fixedBox: false,
fixed: true,
fixedNumber: [1.6, 1],
width: 0,
height: 0,
...this.cropOps
}
},
imgList() {
return this.fileList.map(v => v.url)
},
isSingle() {
return this.limit == 1 && this.isImport
},
showTips() {
return this.isShowTip || this.$slots.tips
},
hasUploaded() {
return this.fileList?.length > 0
},
uploadFile() {
let file = this.fileList?.[0],
size = Number(file.size),
icon = "iconTxt"
//显示大小
if (size > Math.pow(1024, 2)) {
size = (size / Math.pow(1024, 2)).toFixed(1) + 'MB'
} else {
size = (size / 1024).toFixed(1) + 'KB'
}
//显示图标
if (/\.(xls|xlsx)$/.test(file.name)) {
icon = "iconExcel"
} else if (/\.(zip)$/.test(file.name)) {
icon = "iconZip"
} else if (/\.(rar)$/.test(file.name)) {
icon = "iconRar"
} else if (/\.(png)$/.test(file.name)) {
icon = "iconPng"
} else if (/\.(pptx|ppt)$/.test(file.name)) {
icon = "iconPPT"
} else if (/\.(doc|docx)$/.test(file.name)) {
icon = "iconWord"
}
return {...file, size, icon}
}
},
methods: {
onFixedChange(e) {
this.fixedNumber = e === 0 ? [1.6, 1] : [4, 3]
this.$nextTick(() => {
this.$refs.cropper.goAutoCrop()
})
},
handleChange(file, fileList) {
if (this.isImport) {
if (!this.onOverSize(file)) {
this.fileList = []
return false
}
this.fileList = fileList
this.emitChange(fileList)
return false
}
if (this.isCrop) {
if (file.raw.type === 'image/gif') {
this.$message.error(`不支持gif格式的图片`)
this.fileList = []
return false
}
if (!this.onOverSize(file)) {
this.fileList = []
return false
}
this.dialog = true
this.fileList = fileList
} else {
this.fileList = fileList
}
},
handleExceed(files) {
if (this.isSingle && files[0]) {
this.$refs.upload?.clearFiles()
this.$refs.upload?.handleStart(files[0])
} else this.$message.warning(`最多上传${this.limit}${this.isImg ? '图片' : '文件'}`)
},
handlePictureCardPreview(file) {
if (this.fileType !== 'img') return
const index = this.imgList.indexOf(file.url)
const viewer = this.$el.querySelector('.images').$viewer
viewer.view(index)
},
handleRemove(file, fileList) {
this.fileList = fileList
this.emitChange(fileList)
},
emitChange(files) {
this.$emit('change', this.valueIsUrl ? files?.map(e => e.url)?.toString() : files)
},
handleClear() {
this.fileList = []
},
getExtension(name) {
return name.substring(name.lastIndexOf('.'))
},
onOverSize(e) {
const isLt10M = e.size / 1024 / 1024 < this.maxSize
const suffixName = this.getExtension(e.name)
const suffixNameList = this.accept.split(',')
if (suffixNameList.indexOf(`${suffixName.toLowerCase()}`) === -1) {
this.$message.error(`不支持该格式`)
return false
}
if (!isLt10M) {
this.$message.error(`${this.isImg ? '图片' : '文件'}大小不超过${this.maxSize}MB!`)
return false
}
return true
},
onBeforeUpload(event) {
return this.onOverSize(event)
},
onClose() {
this.fileList = []
this.dialog = false
},
submitUpload(file) {
let formData = new FormData()
formData.append('file', file.file)
this.instance.post(this.url, formData, {
withCredentials: false
}).then(res => {
if (res?.code == 0) {
if (this.isWechat) {
this.emitChange([{
...res.data.file,
media: res.data.media
}])
this.fileList.forEach(item => {
if (item.uid === file.file.uid) {
item.id = res.data.file.id
item.path = res.data.file.url
item.url = res.data.file.url,
item.media = res.data.media
}
})
this.emitChange(this.fileList)
this.$message.success('上传成功')
return false
}
let data = res.data[0].split(';')
this.fileList.forEach(item => {
if (item.uid === file.file.uid) {
item.id = data[1]
item.path = data[0]
item.url = data[0]
}
})
this.emitChange(this.fileList)
this.$message.success('上传成功')
}
})
},
previewCrop() {
this.$refs.cropper.getCropBlob(data => {
data.name = this.fileList[0].name;
this.fileList[0].file = new window.File([data], data.name, {type: data.type})
this.fileList[0].file.uid = this.fileList[0].uid
this.submitUpload(this.fileList[0])
this.dialog = false
})
},
/**
* 表单验证
* @param componentName
* @param eventName
* @param params
*/
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
}
}
</script>
<style lang="scss" scoped>
.uploader {
line-height: 1;
::v-deep.el-upload {
width: 100%;
text-align: start;
}
::v-deep.validError {
.el-button {
border-color: #f46;
color: #f46;
}
}
::v-deep.el-upload--picture-card {
border: none;
}
::v-deep .el-list-leave-active, ::v-deep .el-upload-list__item {
transition: all 0s !important;
}
::v-deep.el-upload-list--picture-card .el-upload-list__item {
width: 120px;
height: 120px;
border-radius: 4px;
}
::v-deep.el-upload--picture-card {
width: auto;
height: auto;
}
.el-upload__tip p {
color: #999;
line-height: 16px;
}
::v-deep.fileItem {
width: 100%;
height: 60px;
background: #FFFFFF;
border-radius: 2px;
border: 1px solid #D0D4DC;
display: flex;
align-items: center;
padding: 0 16px;
cursor: default;
.uploadFile {
text-align: start;
flex: 1;
min-width: 0;
display: flex;
color: #222;
.AiIcon {
width: 40px;
height: 40px;
}
.info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
margin-left: 8px;
margin-right: 40px;
line-height: 22px;
}
.size {
color: #888;
}
}
}
.uploaderBox {
width: 120px;
height: 120px;
line-height: 1;
background: #F3F4F7;
border-radius: 2px;
border: 1px solid #D0D4DC;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&:hover {
opacity: 0.6;
}
span {
font-size: 32px;
color: #8899bb;
&:hover {
color: #8899bb;
}
}
p {
margin: 0;
padding-top: 4px;
color: #555;
font-size: 12px;
text-align: center;
line-height: 1;
}
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div class="ai-wrapper">
<ai-title class="w100" v-if="title" :title="title"/>
<div class="ai-wrapper-content" :class="{border}">
<slot/>
</div>
</div>
</template>
<script>
import AiTitle from "./AiTitle";
export default {
name: 'AiWrapper',
components: {AiTitle},
componentName: 'AiWrapper',
provide() {
return {
AiWrapper: this
}
},
props: {
'label-width': {
type: String
},
columnsNumber: {
type: Number,
default: 2
},
border: Boolean,
title: String
},
computed: {
autoWidth() {
return ((1 / this.columnsNumber) * 100).toFixed(2) + '%'
},
autoLableWidth() {
return this.labelWidth
}
},
}
</script>
<style lang="scss" scoped>
.ai-wrapper {
.w100 {
width: 100%;
}
.ai-wrapper-content {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
&.border {
border-left: 1px solid $borderColor;
border-top: 1px solid $borderColor;
.ai-info-item {
border-bottom: 1px solid $borderColor;
border-right: 1px solid $borderColor;
margin-bottom: 0;
line-height: 32px;
.ai-info-item__left {
background: rgba(0, 0, 0, .03);
padding-right: 16px;
border-right: 1px solid $borderColor;
margin-right: 16px;
}
.el-textarea__inner {
border: none;
padding-left: 0;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,368 @@
<template>
<section class="ai-area">
<div v-if="inputClicker" @click="chooseArea" class="input-clicker">
<el-row type="flex" justify="space-between">
<div class="prepend">
<i style="font-size: 16px" class="iconfont iconLocation"/>
切换地区
</div>
<div class="content nowarp-text fill" v-text="fullName"/>
<i class="iconfont iconChange pad-r10"/>
</el-row>
</div>
<el-button v-else-if="!customClicker" class="area-btn" type="primary" size="mini" @click="chooseArea">
{{ btnShowArea ? selectedName : "切换地区" }}
</el-button>
<a class="custom-clicker" v-else @click="chooseArea">
<slot :areaname="selectedName" :fullname="fullName" :id="selected" :areatype="selectedAreaType"/>
</a>
<ai-dialog :visible.sync="dialog" title="选择地区" width="60%" @onConfirm="confirmArea" @open="selected=(value||'')">
<ai-highlight content="您当前选择&nbsp;@v" :value="selectedName" color="#333" bold/>
<div class="area_edge">
<div class="area-box" v-for="ops in showOps">
<h2 v-text="ops.header"/>
<div class="area-item" :class="{selected: selectedMap.includes(area.id)}" v-for="area in ops.list"
@click="getChildrenAreas(area)">
<ai-badge>
<span>{{ area.name }}</span>
<div slot="badge" v-if="showBadge&&area.tipName" :class="getLabelClassByLabelType(area.labelType)">
{{ area.tipName }}
</div>
</ai-badge>
</div>
</div>
</div>
</ai-dialog>
</section>
</template>
<script>
import AiHighlight from "../layout/AiHighlight";
import instance from "../../meta/js/request";
import Area from "../../meta/js/area";
export default {
name: 'AiArea',
components: {AiHighlight},
inject: {
elFormItem: {default: ""},
elForm: {default: ''},
},
model: {
prop: 'value',
event: 'change'
},
props: {
instance: {default: () => instance},
action: String,
areaLevel: {type: [Number, String], default: 5},
btnShowArea: {type: Boolean, default: false},
customClicker: {type: Boolean, default: false},
disabled: {type: Boolean, default: false},
hideLevel: {type: [Number, String], default: 0},
inputClicker: {type: Boolean, default: true},
provinceAction: String,
separator: {type: String, default: ""},
showBadge: {type: Boolean, default: true},
value: String,
valueLevel: {type: [Number, String], default: -1}
},
data() {
return {
selected: null,
areaOps: [],
fullName: '',
dialog: false,
ProvinceCityCounty: [],
}
},
computed: {
currentArea: v => v.selected || v.value,
selectedArea: v => new Area(v.currentArea, v.hashMap),
startLevel: v => Number(v.hideLevel) || 0,//地区最高可选行政地区等级
endLevel: v => Number(v.areaLevel) || 0,//地区最低可选行政地区等级
selectedName: v => v.selectedArea.name || "无",
validateState: v => ['', 'success'].includes(v.elFormItem?.validateState),
selectedMap: v => v.selectedArea.areaMap,
hashMap() {
//地区数据缓存器,用于快速获取数据
const hash = {}
this.areaOps.flat().map(e => hash[e.id] = e)
return hash
},
showOps() {
const levelLabels = {
0: "省/直辖市",
1: "市",
2: "县/区",
3: "乡/镇/街道",
4: "村/社区"
}
let ops = this.areaOps.map((list, i) => ({
header: levelLabels[i], list
})).slice(Math.max(0, this.startLevel), this.endLevel)
if (this.startLevel > 0 && ops.length > 0) {
ops[0].list = ops[0].list.filter(e => e.id == this.selectedMap[this.startLevel])
}
return ops
}
},
watch: {
value: {
immediate: true,
handler(v) {
this.dispatch('ElFormItem', 'el.form.change', [v]);
this.initAreaName()
}
},
},
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
chooseArea() {
if (this.disabled) return
this.selected = this.$copy(this.value)
this.initOptions().then(() => this.dialog = true)
},
confirmArea() {
if (this.valueLevel > -1) {
this.$emit("change", this.selectedMap[this.valueLevel])
} else {
this.$emit("change", this.selected)
}
this.$emit("area", this.selected, this.selectedArea);
this.dialog = false
},
getChildrenAreas(area) {
this.selected = area.id;
const level = Area.getLevelByAreaId(area.id);
if (level < 4) {
this.getAreasByParentId(area.id).then(list => {
this.areaOps.splice(level + 1, 5, list)
})
}
},
getAreasByParentId(id) {
const level = Area.getLevelByAreaId(id)
return new Promise(resolve => {
if (level < 2) {
this.getProvinceCityCounty().then(() => {
resolve(this.ProvinceCityCounty.filter(e => e.parentId == id))
})
} else {
this.instance.post(this.action || "/admin/area/queryAreaByParentId", null, {
withoutToken: true,
params: {id}
}).then(res => {
if (res?.data) {
resolve(res.data)
}
})
}
})
},
getLabelClassByLabelType(type) {
let cls = "badge-label"
switch (type) {
case '1':
cls += ' label-town'
break;
case '3':
cls += ' label-village'
break;
default:
cls += ' label-poor'
break
}
return cls
},
getProvinceCityCounty() {
return new Promise(resolve => {
if (localStorage.getItem("ProvinceCityCounty")) {
resolve(JSON.parse(localStorage.getItem("ProvinceCityCounty")))
} else {
this.instance.post(this.provinceAction || "/admin/area/queryProvinceListContainCity", null, {
withoutToken: true
}).then(res => {
if (res && res.data) {
localStorage.setItem("ProvinceCityCounty", JSON.stringify(res.data))
resolve(res.data)
}
})
}
}).then(list => this.ProvinceCityCounty = list)
},
initOptions() {
this.areaOps = []
let map = {};
return Promise.all([null, ...this.selectedMap].map((id, i) => this.getAreasByParentId(id).then(list => map[i] = list))).then(() => {
this.areaOps = Object.values(map)
})
},
initAreaName() {
if (this.value) {
Area.createByAction(this.currentArea, this.instance).then(names => {
this.selectedArea.getName(names)
this.fullName = this.selectedArea.nameMap.join(this.separator)
this.$emit("update:name", this.selectedName)
this.$emit("fullname", this.fullName)
})
}
}
}
}
</script>
<style lang="scss" scoped>
.ai-area {
.area-btn {
box-shadow: 0 2px 8px 0 rgba(76, 132, 255, 0.6);
}
.input-clicker {
width: 320px;
cursor: pointer;
border: 1px solid #D0D4DC;
line-height: 32px;
border-radius: 2px;
font-size: 14px;
.prepend {
background: rgba(245, 245, 245, 1);
width: auto;
border-right: 1px solid #D0D4DC;
padding: 0 8px;
white-space: nowrap;
}
.content {
text-align: left;
padding-left: 14px;
padding-right: 8px;
direction: rtl;
}
.suffix {
width: auto;
padding: 0 12px
}
&:hover {
border-color: $primaryColor;
}
}
.custom-clicker {
text-decoration: none;
cursor: pointer;
padding: 3px;
}
.area_edge {
max-height: 350px;
overflow-y: auto;
white-space: normal;
}
.area-box {
box-shadow: 0px -1px 0px 0px rgba(238, 238, 238, 1);
padding: 16px 0 8px 0;
& > section {
font-size: 0;
}
& > h2 {
color: rgba(51, 51, 51, 1);
line-height: 22px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
}
.area-item {
display: inline-block;
border-radius: 2px;
border: 1px solid #D0D4DC;
margin: 8px 8px 8px 0;
padding: 3px 10px;
cursor: pointer;
text-align: center;
line-height: normal;
font-size: 14px;
&:hover {
color: rgba($primaryColor, .8);
border-color: rgba($primaryColor, .8);
}
}
a {
text-decoration: none;
border-radius: 4px;
border: 1px solid #ddd;
line-height: normal;
margin: 5px;
padding: 3px;
cursor: pointer;
span {
margin: 0 10px;
}
&:hover {
color: rgba($primaryColor, .8);
border-color: rgba($primaryColor, .8);
}
}
.selected {
color: rgba($primaryColor, .8);
border-color: rgba($primaryColor, .8);
}
}
.badge-label {
font-size: 12px;
font-weight: bold;
border-radius: 15px;
color: #fff;
width: 12px;
letter-spacing: 10px;
text-align: center;
overflow: hidden;
padding: 3px 5px;
white-space: nowrap;
transition: width 1s, letter-spacing 0.05s;
&.label-town {
background: rgba($primaryColor, .8);
}
&.label-village {
background: rgba($primaryColor, .8);
}
&.label-poor {
background: #ffb14c;
}
&:hover {
width: initial;
letter-spacing: unset;
}
}
}
</style>

View File

@@ -0,0 +1,295 @@
<template>
<section class="AiAreaGet">
<el-cascader v-if="refresh" ref="areaCascader" :value="value" size="small" :props="props" :show-all-levels="showAll"
:options="options" @visible-change="editing=true" clearable
filterable :before-filter="handleFindArea" @change="handleAfterFilter"
v-bind="$attrs" v-on="$listeners" popper-class="popperSelectors"/>
</section>
</template>
<script>
/**
* 智能地区选择器
* @displayName AiAreaGet
*/
export default {
name: "AiAreaGet",
inject: {
elFormItem: {default: ""},
},
model: {
prop: 'value',
event: 'change',
},
props: {
/**
* 接口方法类:必填
*/
instance: {default: () => null},
/**
* 绑定地区编码
* @model
*/
value: {default: ""},
/**
* 是否多选
*/
multiple: Boolean,
/**
* 获取地区信息接口地址,默认为:/admin/area/queryAreaByParentId
*/
action: {default: "/admin/area/queryAreaByParentId"},
/**
* 限制获取地区编码的范围,1:省 2:地级市 3:县/区 4:镇/街道 5:村/社区
* @values 1,2,3,4,5
*/
valueLevel: {default: 5},
/**
* 指定根级地区范围
*/
root: {default: ""},
/**
* 获取地区名称,支持.sync 双向获取绑定
*/
name: {default: ""},
/**
* 显示完整地区名称
*/
showAll: Boolean
},
data() {
return {
rules: [10, 8, 6, 3, 0],
cacheOptions: [],
editing: false,
filterData: [],
caches: [],
roots: [],
refresh: true,
rootLoad: ""
}
},
watch: {
value(v) {
!this.editing && !this.caches.includes(this.value) && this.getCacheOptions()
this.dispatch('ElFormItem', 'el.form.change', [v]);
setTimeout(() => this.$emit("update:name", this.$refs.areaCascader?.inputValue))
},
root() {
if (this.value) {
this.getCacheOptions()
} else {
setTimeout(() => {
this.refresh = false
this.$nextTick(() => this.refresh = true)
}, 200)
}
},
options: {
handler() {
this.$nextTick(() => this.$forceUpdate())
}, deep: true, immediate: true
}
},
computed: {
fullArea() {
const length = 12,
getFull = v => this.rules.map(e => {
let reg = new RegExp(`(\\d{${length-e}})\\d{${e}}`, 'g')
return v?.replace(reg, '$1' + Array(e).fill(0).join(''))
}).filter((e, i) => i <= this.getLevel(v))
return this.multiple ? [this.value].flat()?.map(e => getFull(e)) : getFull(this.value)
},
props() {
return {
label: 'name',
value: 'id',
lazy: true,
multiple: this.multiple,
checkStrictly: true,
emitPath: false,
lazyLoad: (node, resolve) => {
if (!(this.caches.includes(node.value) && this.fullArea.includes(node.value)) || node.loading) {
if (node?.level == 0) {
this.getRoots(resolve, "lazyLoad")
} else if (node?.level > 0 && node.children?.length == 0) {
let {id, leaf} = node.data
leaf ? resolve([]) : this.getAreasByParent(id, resolve)
} else resolve([])
} else resolve([])
}
}
},
options() {
return [...this.cacheOptions, ...this.filterData]
},
filtering() {
let v = this.$refs?.areaCascader?.filtering
if (!v) this.filterData = []
return v
}
},
methods: {
getLevel(code) {
let lv = -1
this.rules.some((e, index) => {
let reg = new RegExp(`0{${e}}$`, "g")
if (reg.test(code)) {
lv = index
return true
}
})
return lv
},
getRoots(resolve, from) {
let url = '/admin/area/queryProvinceList'
if (this.root) {
url = "/admin/area/queryAreaByAreaid"
if (this.rootLoad == this.root) {
let waitRoots = (count = 0) => setTimeout(() => {
if (this.roots.length > 0 || count == 5) {
resolve(this.roots)
} else waitRoots(++count)
}, 500)
return from == "lazyLoad" ? '' : waitRoots()
}
}
this.rootLoad = JSON.parse(JSON.stringify(this.root))
if (this.roots.some(e => e.id == this.root)) {
resolve(this.roots)
} else this.instance.post(url, null, {
params: {id: this.root, from}, withoutToken: true
}).then(res => {
if (res?.data) {
this.roots = [res.data].flat().map(e => ({...e, leaf: e.type == this.valueLevel}))
resolve(this.roots)
}
})
},
getAreasByParent(id, resolve) {
id && this.instance.post(this.action, null, {
params: {id}, withoutToken: true,
}).then(res => {
if (res?.data) {
resolve(res.data.map(e => ({...e, leaf: e.type == this.valueLevel})))
}
})
},
async getCacheOptions() {
let finished = 0
const hasChild = ids => ids?.some(e => this.fullArea?.flat()?.includes(e)),
appendChildren = (area, resolve) => {
let values = [this.value].flat()
if (values.includes(area.id)) {
finished++
if (finished == values.length) {
this.$emit("update:name", area.name)
resolve()
}
} else this.getAreasByParent(area.id, data => {
this.$set(area, "children", data)
data.map(d => {
this.caches.push(d.id)
hasChild([d.id]) && appendChildren(d, resolve)
})
})
}
if (!!this.value?.toString()) {
this.cacheOptions = []
this.caches = []
await this.getRoots(data => {
this.caches = data?.map(e => e.id) || []
new Promise(resolve => {
if (hasChild(data.map(e => e.id))) {
data.map(e => hasChild([e.id]) && appendChildren(e, resolve))
} else resolve()
}).then(() => {
this.cacheOptions = data
})
}, "initWithValue")
} else if (!!this.root) {
this.caches = []
await this.getRoots(data => {
this.caches = data?.map(e => e.id) || []
new Promise(resolve => {
if (hasChild(data.map(e => e.id))) {
data.map(e => hasChild([e.id]) && appendChildren(e, resolve))
} else resolve()
}).then(() => {
this.cacheOptions = data
})
}, "initWithRoot")
}
},
/**
* 表单验证
* @param componentName
* @param eventName
* @param params
*/
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
handleFindArea(areaName) {
return new Promise(resolve => {
this.instance.post("/admin/area/queryAreaByAreaName", null, {
params: {areaName}
}).then(res => {
if (res?.data) {
let range = new RegExp(`^${this.root.replace(/0+$/g, '')||'\d'}`)
this.filterData = res.data.filter(e => !this.caches.includes(e.id) && range.test(e.id)).map(e => ({
...e,
leaf: e.type == this.valueLevel
}))
resolve()
}
})
})
},
handleAfterFilter(v) {
this.$emit('select', this.$refs.areaCascader?.getCheckedNodes(true))
this.$refs.areaCascader?.toggleDropDownVisible(false)
if (!this.multiple) {
if (this.filterData?.length > 0) {
this.filterData = []
}
this.editing = this.caches.includes(v);
}
}
},
created() {
setTimeout(() => {
this.cacheOptions.length == 0 && this.getCacheOptions()
})
}
}
</script>
<style lang="scss" scoped>
.AiAreaGet {
width: 100%;
.el-cascader {
width: 100%;
}
}
</style>
<style lang="scss">
.popperSelectors {
.el-cascader-menu__wrap {
height: 300px;
}
}
</style>

View File

@@ -0,0 +1,273 @@
<template>
<section class="AiAreaSelect">
<el-row type="flex" :gutter="5">
<el-col v-for="(item,i) in areaInfo" :key="i" v-show="isShowSelector(i)" style="width: auto;">
<el-select :size="selectClass" v-if="areaLists[i].length>0||alwaysShow" @focus="$emit('focus')"
@blur="$emit('blur')" :placeholder="placeVal[i]" clearable
:disabled="isDisabledSelector(i)" :value="areaInfo[i]" @change="v=>handleSelectorChange(v,i)">
<template v-if="lockFirstOption&&i==(hideNum||0)">
<el-option v-if="!areaInfo[i]||areaInfo[i]===area.id" v-for="(area,j) in areaLists[i]" :key="j"
:value="area.id" :label="area.name"/>
</template>
<el-option v-else v-for="(area,j) in areaLists[i]" :key="j" :value="area.id" :label="area.name"/>
</el-select>
</el-col>
</el-row>
</section>
</template>
<script>
export default {
name: "AiAreaSelect",
model: {
prop: 'value',
event: 'change',
},
inject: {
elFormItem: {default: ""},
},
props: {
value: String,
areaLevel: {type: [Number, String], default: 5},
disabledLevel: {type: [Number, String], default: 0},
disabled: {type: Boolean, default: false},
hideLevel: {type: [Number, String], default: 0},
valueLevel: {type: [Number, String], default: -1},
alwaysShow: {type: Boolean, default: false},
selectClass: {type: String, default: 'small'},
lockFirstOption: {type: Boolean, default: false},
instance: Function,
action: String,
provinceAction: String
},
computed: {
hideNum() {
return Number(this.hideLevel)
},
disabledNum() {
return Number(this.disabledLevel)
},
showNum() {
return Number(this.areaLevel)
},
valueIndex() {
return Number(this.valueLevel)
},
current() {
return this.selected || this.value
},
areaInfo() {
let info = {}
const currentLevel = this.getLevelByAreaId(this.current)
for (let i = 0; i < this.showNum; i++) {
//防止地区代码出现,在获取到选项后再赋值
if (i <= currentLevel) info[i] = this.areaLists[i]?.length ? this.getAreaByAreaType(i) : ""
else info[i] = null
}
return info
},
areaObj() {
let info = {}
for (let key in this.areaInfo) {
if (this.areaInfo[key]) {
info[key] = this.areaLists[key].find(e => e.id === this.areaInfo[key])
}
}
return info
},
selectedName() {
const index = this.getLevelByAreaId(this.current)
if (index > -1) {
const area = this.areaLists[index].find(e => e.id === this.current)
return area ? area.name : ""
} else return ""
},
selectedFullName() {
let name = ""
for (let i in this.areaInfo) {
if (this.areaInfo[i]) {
const area = this.areaLists[i].find(e => e.id === this.areaInfo[i])
name += area ? area.name : ""
}
}
return name
},
areaLelve() {
return this.getLevelByAreaId(this.current);
}
},
watch: {
selected() {
if (this.valueIndex > -1) {
this.$emit("change", this.areaInfo[this.valueIndex] || null)
} else {
this.$emit("change", this.current)
}
this.$emit("area", this.areaInfo);
this.$emit('areaLelve', this.areaLelve)
},
areaObj() {
this.$emit("areaObj", this.areaObj)
},
selectedName() {
this.$emit("name", this.selectedName)
},
selectedFullName() {
this.$emit("fullname", this.selectedFullName)
},
valueLevel() {
this.refreshAreaList();
},
value(v) {
//特殊处置结果 后台传值赋值改变需要重新加载数据
this.dispatch('ElFormItem', 'el.form.change', [v]);
if (!this.selected) {
this.refreshAreaList();
}
}
},
data() {
return {
selected: null,
areaLists: {
0: [],
1: [],
2: [],
3: [],
4: [],
},
placeVal: {
0: '请选择省',
1: '请选择市',
2: '请选择区',
3: '请选择镇',
4: '请选择村'
},
ProvinceCityCounty: []
}
},
methods: {
isShowSelector(i) {
let index = Number(i)
return index >= this.hideNum
},
isDisabledSelector(i) {
let index = Number(i)
return this.disabled || (index < this.disabledNum)
},
handleSelectorChange(area, index) {
if (area) {
this.selected = area
if (index < 4 && this.areaInfo[index]) {
this.getAreasByParentId(index).then(() => {
for (let i = index; i < this.showNum; i++) {
if (this.areaLists[i + 2]) this.areaLists[i + 2] = []
}
})
}
} else {//清空操作
let i = Number(index), pre = i - 1, origin = JSON.parse(JSON.stringify(this.areaInfo))
if (pre > 0) {
this.selected = origin[pre]
for (let j = i + 1; j < this.showNum; j++) {
this.areaLists[j] = []
}
} else {
this.selected = origin[0]
for (let j = i + 2; j < this.showNum; j++) {
this.areaLists[j] = []
}
}
}
},
getAreasByParentId(index) {
index = Number(index)
return new Promise((resolve) => {
this.areaLists[index + 1] = []
if (index < 2) {
index = Number(index)
let data = this.ProvinceCityCounty.filter(e => e.parentId == this.areaInfo[index])
if (data && (index + 1 < this.showNum)) this.areaLists[index + 1] = data
this.$forceUpdate();
resolve(index)
} else {
this.instance.post(this.action || "/admin/area/queryAreaByParentId", null, {
withoutToken: true,
params: {id: this.areaInfo[index]}
}).then(res => {
if (res?.data) {
index = Number(index)
if (index + 1 < this.showNum) this.areaLists[index + 1] = res.data
this.$forceUpdate()
}
resolve(index)
})
}
})
},
getProvinceCityCounty() {
return this.instance.post(this.provinceAction || "/admin/area/queryProvinceListContainCity", null, {withoutToken: true})
},
getAreaByAreaType(areaType) {
let lvCount = [2, 4, 6, 9, 12]
return this.current?.split("").map((e, i) => i < lvCount[areaType] ? e : 0).join("")
},
getLevelByAreaId(code) {
if (code) {
if (code.length == 2 || code.endsWith('0000000000')) return 0
else if (code.endsWith('00000000')) return 1
else if (code.endsWith('000000')) return 2
else if (code.endsWith('000')) return 3
else return 4
} else return -1
},
refreshAreaList() {
for (let i = 0; i < this.showNum; i++) {
this.areaLists[i] = []
}
this.getProvinceCityCounty().then(res => {
this.ProvinceCityCounty = res.data
this.areaLists[0] = res.data.filter(d => !d.parentId)
const getAreaList = i => {
if (this.areaInfo?.[i])
this.getAreasByParentId(i).then(next => getAreaList(next + 1))
}
getAreaList(0)
})
},
/**
* 表单验证
* @param componentName
* @param eventName
* @param params
*/
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
},
created() {
this.refreshAreaList()
}
}
</script>
<style lang="scss" scoped>
.AiAreaSelect {
.el-col {
min-width: 90px;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More