提交一波轮子

This commit is contained in:
2023-01-07 12:04:42 +08:00
parent d8d83395a9
commit d9e765386a
14 changed files with 860 additions and 1 deletions

View File

@@ -6,7 +6,7 @@ const start = () => {
easycom: {
autoscan: true,
custom: {
"^(K|V)(.*)": "@/components/$1$2.vue",
"^(Ai|V)(.*)": "@/components/$1$2.vue",
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},

View File

@@ -22,6 +22,10 @@
"@dcloudio/uni-mp-weixin": "3.0.0-3061420221215001",
"@dcloudio/uni-quickapp-webview": "3.0.0-3061420221215001",
"@dcloudio/uni-ui": "^1.4.23",
"axios": "^1.2.2",
"axios-miniprogram-adapter": "^0.3.5",
"dayjs": "^1.11.7",
"query-string": "^8.1.0",
"vue": "^3.2.45",
"vue-i18n": "^9.1.9"
},

View File

@@ -0,0 +1,61 @@
<template>
<section class="AiGroup" :class="{noBorder,description}" :style="{paddingLeft:left+'rpx'}">
<div class="groupHeader" v-if="title" v-text="title"/>
<slot/>
</section>
</template>
<script>
export default {
name: "AiGroup",
provide() {
return {
labelColor: this.labelColor,
description: this.description,
activeStep: this.activeStep
}
},
props: {
title: String,
noBorder: Boolean,
left: {default: 32},
labelColor: {default: "#333"},
description: {default: false}, //用于展示则不会又红星,会把标签的内间距去掉
activeStep: {default: 1}//用于步骤组件的当前步数
},
data() {
return {
items: []
}
}
}
</script>
<style lang="scss" scoped>
.AiGroup {
background: #FFFFFF;
box-shadow: inset 0px -1px 0px 0px #DDDDDD;
&.noBorder {
box-shadow: none;
}
& + .AiGroup {
margin-top: 16px;
}
.groupHeader {
font-weight: bold;
font-size: 36px;
padding-left: 20px;
margin-bottom: 16px;
}
&.description {
.groupHeader {
padding-left: 0;
}
}
}
</style>

View File

@@ -0,0 +1,124 @@
<template>
<section class="AiItem" :class="{border,readonly}">
<div v-if="topLabel" class="topLabel">
<div class="labelPane flex">
<div class="label" :class="{required,labelBold}" :style="{color}" v-text="label"/>
<slot name="sub" v-if="$slots.sub"/>
</div>
<div class="itemContent">
<slot v-if="$slots.default"/>
<div v-else v-text="value"/>
</div>
</div>
<div v-else class="normal flex">
<div class="fill flex">
<div class="label" :class="{required,labelBold}" :style="{color}" v-text="label"/>
<slot name="sub" v-if="$slots.sub"/>
</div>
<div class="flexContent">
<slot v-if="$slots.default"/>
<div v-else v-text="value"/>
</div>
</div>
</section>
</template>
<script>
export default {
name: "AiItem",
inject: {
labelColor: {default: "#333"},
description: {default: false}
},
props: {
value: {default: ""},
label: {default: ""},
required: Boolean,
topLabel: Boolean,
border: {default: true},
labelBold: Boolean,
},
computed: {
color: v => v.labelColor,
readonly: v => !!v.description
}
}
</script>
<style lang="scss" scoped>
.AiItem {
font-family: PingFangSC-Regular, PingFang SC;
&.border {
.normal {
border-bottom: 2px solid #ddd;
}
.topLabel {
border-bottom: 2px solid #ddd;
}
}
.normal {
width: 100%;
padding-right: 32px;
box-sizing: border-box;
height: 112px;
.flexContent {
max-width: 62vw;
}
}
.label {
padding-left: 20px;
font-weight: 400;
margin-right: 20px;
position: relative;
&.required:before {
position: absolute;
display: block;
left: 0;
top: 50%;
transform: translateY(-50%);
content: "*";
color: #f46;
}
&.labelBold {
font-weight: bold;
font-size: 34px;
}
}
::v-deep.topLabel {
padding: 32px 32px 32px 0;
.labelPane {
margin-bottom: 32px;
}
.itemContent {
padding-left: 20px;
.AiMore > .u-icon {
width: 100%;
}
}
}
//展示模式下的特有样式
&.readonly {
.label, .itemContent {
padding-left: 0;
}
}
.AiStep:last-of-type {
.stepLine {
display: none
}
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<section class="AiPagePicker">
<div @click="handleJump">
<slot v-if="$slots.default"/>
<div v-else v-text="selectedLabel"/>
</div>
</section>
</template>
<script>
import qs from 'query-string'
export default {
name: "AiPagePicker",
model: {
prop: "value",
event: "input"
},
props: {
value: {default: ""},
type: {default: "sysUser"},
nodeKey: {default: "id"},
selected: {default: () => []},
placeholder: {default: "请选择"},
ops: {default: () => ({})},
valueObj: Boolean,
params: {default: () => ({})},
multiple: Boolean
},
data() {
return {
configList: {
sysUser: {url: "/components/pages/selectSysUser", label: "name"},
custom: {...this.ops}
},
}
},
computed: {
config() {
return this.configList[this.type] || {}
},
selectedLabel() {
let {placeholder, config: {label}} = this
return this.selected?.map(e => e[label])?.toString() || placeholder
}
},
methods: {
handleJump() {
let {config, nodeKey, valueObj, multiple} = this,
selected = (valueObj ? this.value[nodeKey] : this.value) || this.selected?.map(e => e[nodeKey])
uni.$once('pagePicker:' + this.type, data => {
console.log('发送', data)
this.$emit("update:selected", data)
this.$emit("select", data)
this.$emit("input", valueObj ? data :
data ? (multiple ? [data].flat()?.map(e => e[nodeKey]) : data[nodeKey]) : "")
})
let url = `${config.url}`,
qsstr = qs.stringify({
selected, nodeKey, multiple, ...this.params
})
if (!!qsstr) {
url += `?${qsstr}`
}
uni.navigateTo({url})
}
}
}
</script>
<style lang="scss" scoped>
.AiPagePicker {
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<section class="submitEvaluation">
<AiGroup>
<AiItem label="评价分数" top-label required>
<uni-rate v-model="form.score" :size="64" active-color="#F8B425" :min-count="1" inactive-icon="star-fill"/>
</AiItem>
</AiGroup>
<u-gap height="24"/>
<AiGroup>
<AiItem label="评价详情" top-label required>
<uni-easyinput type="textarea" v-model="form.content" placeholder="请简要描述..."/>
</AiItem>
</AiGroup>
<u-gap height="24"/>
<AiGroup>
<AiItem label="附件" top-label>
<uni-file-picker file-mediatype="all" v-model="form.files" :limit="9" mode="grid"/>
</AiItem>
</AiGroup>
<div class="fixed-bottom">
<div class="bottomBtn" @click="submit">提交</div>
</div>
</section>
</template>
<script>
export default {
name: "submitEvaluation",
appName: "提交评价",
data() {
return {
form: {
files: []
}
}
},
methods: {
submit() {
if (!this.form.score) {
return this.$toast("请选择评价分数!")
}
if (!this.form.content) {
return this.$toast("请填写评价详情!")
}
this.$instance.post("/app/appbusinesscompletionevaluation/addOrUpdate", this.form).then(res => {
if (res?.code == 0) {
this.$toast("提交成功!")
setTimeout(() => uni.navigateBack({}), 1500)
}
})
}
},
onLoad(params) {
this.form.bizId = params.bid
}
}
</script>
<style lang="scss" scoped>
.submitEvaluation {
}
</style>

View File

@@ -1,8 +1,10 @@
import {createSSRApp} from "vue";
import App from "./App.vue";
import util from "./utils/util";
export function createApp() {
const app = createSSRApp(App);
Object.keys(util).map(e => app.config.globalProperties[e] = util[e])
return {
app,
};

19
wxmp/src/utils/coin.js Normal file
View File

@@ -0,0 +1,19 @@
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)
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(/^元/, "零元")
}
}

56
wxmp/src/utils/dict.js Normal file
View File

@@ -0,0 +1,56 @@
export default {
instance: null,
init(instance) {
this.instance = instance
},
dicts() {
return uni.getStorageSync('dicts') || [];
},
load(...code) {
return !!this.instance && this.instance.post('/admin/dictionary/queryValsByCodeList?codeList=' + code.join(','), null, {
withoutToken: true
}).then((res) => {
if (res && res.data) {
let cacheDicts = {},
meta = {};
this.dicts().map((e) => (cacheDicts[e.key] = e));
res.data.map((e) => (meta[e.key] = e));
let dicts = {...cacheDicts, ...meta};
uni.setStorageSync('dicts', Object.values(dicts));
}
});
},
getDict(key) {
if (this.dicts().length) {
let dict = this.dicts().find((e) => e.key == key);
return dict ? dict.values : [];
} else return [];
},
getValue(key, label) {
if (this.dicts().length) {
let dict = this.dicts().find((e) => e.key == key);
if (dict) {
let item = dict.values.find((v) => v.dictName == label);
return item ? item.dictValue : label;
} else return label;
} else return label;
},
getLabel(key, value) {
if (this.dicts().length) {
let dict = this.dicts().find((e) => e.key == key);
if (dict) {
let item = dict.values.find((v) => v.dictValue == value);
return item ? item.dictName : value;
} else return value ? value : '';
} else return value ? value : '';
},
getColor(key, value) {
if (this.dicts().length) {
let dict = this.dicts().find((e) => e.key == key);
if (dict) {
let item = dict.values.find((v) => v.dictValue == value);
return item ? item.dictColor : value;
} else return value;
} else return value;
}
}

29
wxmp/src/utils/http.js Normal file
View File

@@ -0,0 +1,29 @@
import axios from 'axios'
import adapter from 'axios-miniprogram-adapter'
const instance = axios.create({
timeout: 600000,
withCredentials: true,
adapter
})
const getToken = () => {
let vuex = uni.getStorageSync("vuex")
return !!vuex ? JSON.parse(vuex).token : null
}
const source = axios.CancelToken.source();
instance.interceptors.request.use(config => {
if (config.withoutToken) {
return config
} else if (getToken()) {
config.headers["Authorization"] = getToken()
} else {
config.cancelToken = source.token
source.cancel("用户未验证,取消请求:" + config.url)
}
return config
}, err => {
console.error(err)
return Promise.reject(err)
})
export default instance

238
wxmp/src/utils/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
}
}
}

35
wxmp/src/utils/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

15
wxmp/src/utils/regular.js Normal file
View File

@@ -0,0 +1,15 @@
export default {
phone: /^((0\d{2,3}-\d{7,8})|((13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}))$/,
password: /^(?=.*\d)(?=.*[a-zA-Z])(?=.*[~!@#$%^&*,.?_-])[\da-zA-Z~!@#$%^&*,.?_-]{8,16}$/,
money: /^([1-9]\d*|0)(\.\d{1,2})?$/,
area: {
village: /^\d{9}[^0]0{0,2}$/,
town: /^\d{6}[^0]0{0,2}000$/,
country: /^\d{4}[^0]0?0{6}$/,
city: /^\d{2}[^0]0?0{8}$/,
province: /^[^0]0?0{10}$/,
},
zh: /^[\u4e00-\u9fa5]+$/,
email: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
ip: /((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))/
}

140
wxmp/src/utils/util.js Normal file
View File

@@ -0,0 +1,140 @@
import $dayjs from './moment'
import $dict from './dict'
import $qs from 'query-string'
import $coin from './coin'
import $reg from "./regular"
import identity from "./identity"
const $toast = (obj) => {
let params = {title: '', duration: 2000, icon: 'none'};
if (typeof obj == 'string') {
params.title = obj;
} else {
Object.assign(params, obj);
}
uni.showToast(params);
};
const $loading = (title = "加载中") => {
uni.showLoading({
title: title || '加载中',
mask: true
});
};
const $hideLoading = () => {
uni.hideLoading();
};
const $dialog = {
alert: (params) => {
return new Promise((resolve) => {
uni.showModal({
title: '温馨提示',
showCancel: false,
confirmColor: '#197DF0',
confirmText: params?.confirmButtonText || '确定',
...params,
success: (res) => {
if (res.confirm) {
resolve();
}
}
});
});
},
confirm: (params) => {
return new Promise((resolve, reject) => {
uni.showModal({
title: '温馨提示',
showCancel: true,
confirmColor: '#197DF0',
cancelText: params.cancelButtonText ? params.cancelButtonText : '取消',
confirmText: params.confirmButtonText ? params.confirmButtonText : '确定',
...params,
success: (res) => {
if (res.confirm) {
resolve();
} else if (res.cancel) {
reject();
}
}
});
});
}
};
const $linkTo = (url) => {
uni.navigateTo({
url
});
};
const $formatName = (name) => {
if (name === undefined) {
return;
}
return name.substr(name.length - 2, name.length > 2 ? name.length - 1 : name.length);
};
const $previewImage = (list, index, urlName) => {
uni.previewImage({
current: list[index][urlName],
urls: list.map((v) => v[urlName])
});
};
const $getUserProfile = () => {
return new Promise(function (resolve) {
wx.getUserProfile({
desc: '用于完善会员资料',
lang: 'zh_CN',
success: (data) => {
resolve(data);
},
fail: (err) => {
console.log(err);
}
});
});
};
/**
* 获取code
* @returns {Promise<unknown>}
*/
const $getLoginCode = () => {
return new Promise(function (resolve, reject) {
uni.login({
success: function (res) {
if (res.code) {
resolve(res);
} else {
reject(res);
}
},
fail: function () {
reject(false);
}
});
});
};
export default {
$toast,
$loading,
$hideLoading,
$dialog,
$linkTo,
$formatName,
$previewImage,
$getUserProfile,
$dayjs,
$dict,
$getLoginCode,
$qs,
$coin,
$reg,
$ChID: identity
};