Последняя версия с сервера прошлого разработчика

This commit is contained in:
2025-07-10 04:35:51 +00:00
commit c731570032
1174 changed files with 134314 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
<template>
<loading-card :loading="loading" class="px-6 py-4">
<h3 class="flex mb-3 text-base text-80 font-bold">
{{ title }}
<span class="ml-auto font-semibold text-70 text-sm"
>({{ formattedTotal }} {{ __('total') }})</span
>
</h3>
<div v-if="helpText" class="absolute pin-r pin-b p-2">
<tooltip trigger="hover" placement="top-start">
<icon
type="help"
viewBox="0 0 17 17"
height="16"
width="16"
class="cursor-pointer text-60 -mb-1"
/>
<tooltip-content
slot="content"
v-html="helpText"
:max-width="helpWidth"
/>
</tooltip>
</div>
<div class="min-h-90px">
<div class="overflow-hidden overflow-y-auto max-h-90px">
<ul class="list-reset">
<li
v-for="item in formattedItems"
class="text-xs text-80 leading-normal"
>
<span
class="inline-block rounded-full w-2 h-2 mr-2"
:style="{
backgroundColor: item.color,
}"
/>{{ item.label }} ({{ item.value }} - {{ item.percentage }}%)
</li>
</ul>
</div>
<div
ref="chart"
:class="chartClasses"
style="
width: 90px;
height: 90px;
right: 20px;
bottom: 30px;
top: calc(50% + 15px);
"
/>
</div>
</loading-card>
</template>
<script>
import Chartist from 'chartist'
import 'chartist/dist/chartist.min.css'
const colorForIndex = index =>
[
'#F5573B',
'#F99037',
'#F2CB22',
'#8FC15D',
'#098F56',
'#47C1BF',
'#1693EB',
'#6474D7',
'#9C6ADE',
'#E471DE',
][index]
export default {
name: 'PartitionMetric',
props: {
loading: Boolean,
title: String,
helpText: {},
helpWidth: {},
chartData: Array,
},
data: () => ({ chartist: null }),
watch: {
chartData: function (newData, oldData) {
this.renderChart()
},
},
mounted() {
this.chartist = new Chartist.Pie(
this.$refs.chart,
this.formattedChartData,
{
donut: true,
donutWidth: 10,
donutSolid: true,
startAngle: 270,
showLabel: false,
}
)
this.chartist.on('draw', context => {
if (context.type === 'slice') {
context.element.attr({
style: `fill: ${context.meta.color} !important`,
})
}
})
},
methods: {
renderChart() {
this.chartist.update(this.formattedChartData)
},
getItemColor(item, index) {
return typeof item.color === 'string' ? item.color : colorForIndex(index)
},
},
computed: {
chartClasses() {
return [
'vertical-center',
'rounded-b-lg',
'ct-chart',
'mr-4',
this.currentTotal <= 0 ? 'invisible' : '',
]
},
formattedChartData() {
return { labels: this.formattedLabels, series: this.formattedData }
},
formattedItems() {
return _(this.chartData)
.map((item, index) => {
return {
label: item.label,
value: item.value,
color: this.getItemColor(item, index),
percentage:
this.currentTotal > 0
? Nova.formatNumber(
new String(
((item.value * 100) / this.currentTotal).toFixed(2)
)
)
: '0',
}
})
.value()
},
formattedLabels() {
return _(this.chartData)
.map(item => item.label)
.value()
},
formattedData() {
return _(this.chartData)
.map((item, index) => {
return {
value: item.value,
meta: { color: this.getItemColor(item, index) },
}
})
.value()
},
formattedTotal() {
let total = this.currentTotal.toFixed(2)
let roundedTotal = Math.round(total)
if (roundedTotal.toFixed(2) == total) {
return Nova.formatNumber(new String(roundedTotal))
}
return Nova.formatNumber(new String(total))
},
currentTotal() {
return _.sumBy(this.chartData, 'value')
},
},
}
</script>

View File

@@ -0,0 +1,211 @@
<template>
<loading-card :loading="loading" class="px-6 py-4">
<div class="flex mb-4">
<h3 class="mr-3 text-base text-80 font-bold">{{ title }}</h3>
<div v-if="helpText" class="absolute pin-r pin-b p-2 z-20">
<tooltip trigger="click" placement="top-start">
<icon
type="help"
viewBox="0 0 17 17"
height="16"
width="16"
class="cursor-pointer text-60 -mb-1"
/>
<tooltip-content
slot="content"
v-html="helpText"
:max-width="helpWidth"
/>
</tooltip>
</div>
<select
v-if="ranges.length > 0"
@change="handleChange"
class="select-box-sm ml-auto min-w-24 h-6 text-xs appearance-none bg-40 pl-2 pr-6 active:outline-none active:shadow-outline focus:outline-none focus:shadow-outline"
>
<option
v-for="option in ranges"
:key="option.value"
:value="option.value"
:selected="selectedRangeKey == option.value"
>
{{ option.label }}
</option>
</select>
</div>
<p class="flex items-center text-4xl mb-4">
{{ formattedValue }}
<span v-if="suffix" class="ml-2 text-sm font-bold text-80">{{
formattedSuffix
}}</span>
</p>
<div
ref="chart"
class="absolute pin rounded-b-lg ct-chart"
style="top: 60%"
/>
</loading-card>
</template>
<script>
import _ from 'lodash'
import Chartist from 'chartist'
import 'chartist-plugin-tooltips'
import 'chartist/dist/chartist.min.css'
import { SingularOrPlural } from 'laravel-nova'
import 'chartist-plugin-tooltips/dist/chartist-plugin-tooltip.css'
export default {
name: 'BaseTrendMetric',
props: {
loading: Boolean,
title: {},
helpText: {},
helpWidth: {},
value: {},
chartData: {},
maxWidth: {},
prefix: '',
suffix: '',
suffixInflection: true,
ranges: { type: Array, default: () => [] },
selectedRangeKey: [String, Number],
format: {
type: String,
default: '0[.]00a',
},
},
data: () => ({
chartist: null,
resizeObserver: null,
}),
watch: {
selectedRangeKey: function (newRange, oldRange) {
this.renderChart()
},
chartData: function (newData, oldData) {
this.renderChart()
},
},
created() {
const debouncer = _.debounce(callback => callback(), Nova.config.debounce)
this.resizeObserver = new ResizeObserver(entries => {
debouncer(() => {
this.renderChart()
})
})
},
mounted() {
const low = Math.min(...this.chartData)
const high = Math.max(...this.chartData)
// Use zero as the graph base if the lowest value is greater than or equal to zero.
// This avoids the awkward situation where the chart doesn't appear filled in.
const areaBase = low >= 0 ? 0 : low
this.chartist = new Chartist.Line(this.$refs.chart, this.chartData, {
lineSmooth: Chartist.Interpolation.none(),
fullWidth: true,
showPoint: true,
showLine: true,
showArea: true,
chartPadding: {
top: 10,
right: 0,
bottom: 0,
left: 0,
},
low,
high,
areaBase,
axisX: {
showGrid: false,
showLabel: true,
offset: 0,
},
axisY: {
showGrid: false,
showLabel: true,
offset: 0,
},
plugins: [
Chartist.plugins.tooltip({
anchorToPoint: true,
transformTooltipTextFnc: value => {
let formattedValue = Nova.formatNumber(
new String(value),
this.format
)
if (this.prefix) {
return `${this.prefix}${formattedValue}`
}
if (this.suffix) {
const suffix = this.suffixInflection
? SingularOrPlural(value, this.suffix)
: this.suffix
return `${formattedValue} ${suffix}`
}
return `${formattedValue}`
},
}),
],
})
this.resizeObserver.observe(this.$refs.chart)
},
beforeDestroy() {
this.resizeObserver.unobserve(this.$refs.chart)
},
methods: {
renderChart() {
this.chartist.update(this.chartData)
},
handleChange(event) {
this.$emit('selected', event.target.value)
},
},
computed: {
isNullValue() {
return this.value == null
},
formattedValue() {
if (!this.isNullValue) {
const value = Nova.formatNumber(new String(this.value), this.format)
return `${this.prefix}${value}`
}
return ''
},
formattedSuffix() {
if (this.suffixInflection === false) {
return this.suffix
}
return SingularOrPlural(this.value, this.suffix)
},
},
}
</script>

View File

@@ -0,0 +1,202 @@
<template>
<loading-card :loading="loading" class="px-6 py-4">
<div class="flex mb-4">
<h3 class="mr-3 text-base text-80 font-bold">{{ title }}</h3>
<div v-if="helpText" class="absolute pin-r pin-b p-2 z-20">
<tooltip trigger="click" placement="top-start">
<icon
type="help"
viewBox="0 0 17 17"
height="16"
width="16"
class="cursor-pointer text-60 -mb-1"
/>
<tooltip-content
slot="content"
v-html="helpText"
:max-width="helpWidth"
/>
</tooltip>
</div>
<select
v-if="ranges.length > 0"
@change="handleChange"
class="select-box-sm ml-auto min-w-24 h-6 text-xs appearance-none bg-40 pl-2 pr-6 active:outline-none active:shadow-outline focus:outline-none focus:shadow-outline"
>
<option
v-for="option in ranges"
:key="option.value"
:value="option.value"
:selected="selectedRangeKey == option.value"
>
{{ option.label }}
</option>
</select>
</div>
<p class="flex items-center text-4xl mb-4">
{{ formattedValue }}
<span v-if="suffix" class="ml-2 text-sm font-bold text-80">{{
formattedSuffix
}}</span>
</p>
<div>
<p class="flex items-center text-80 font-bold">
<svg
v-if="increaseOrDecreaseLabel == 'Decrease'"
xmlns="http://www.w3.org/2000/svg"
class="text-danger stroke-current mr-2"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"
/>
</svg>
<svg
v-if="increaseOrDecreaseLabel == 'Increase'"
class="text-success stroke-current mr-2"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
<span v-if="increaseOrDecrease != 0">
<span v-if="growthPercentage !== 0">
{{ growthPercentage }}%
{{ __(increaseOrDecreaseLabel) }}
</span>
<span v-else> {{ __('No Increase') }} </span>
</span>
<span v-else>
<span v-if="previous == '0' && value != '0'">
{{ __('No Prior Data') }}
</span>
<span v-if="value == '0' && previous != '0' && !zeroResult">
{{ __('No Current Data') }}
</span>
<span v-if="value == '0' && previous == '0' && !zeroResult">
{{ __('No Data') }}
</span>
</span>
</p>
</div>
</loading-card>
</template>
<script>
import { SingularOrPlural } from 'laravel-nova'
export default {
name: 'BaseValueMetric',
props: {
loading: { default: true },
title: {},
helpText: {},
helpWidth: {},
maxWidth: {},
previous: {},
value: {},
prefix: '',
suffix: '',
suffixInflection: {
default: true,
},
selectedRangeKey: [String, Number],
ranges: { type: Array, default: () => [] },
format: {
type: String,
default: '(0[.]00a)',
},
zeroResult: {
default: false,
},
},
methods: {
handleChange(event) {
this.$emit('selected', event.target.value)
},
},
computed: {
growthPercentage() {
return Math.abs(this.increaseOrDecrease)
},
increaseOrDecrease() {
if (this.previous == 0 || this.previous == null || this.value == 0)
return 0
return (((this.value - this.previous) / this.previous) * 100).toFixed(2)
},
increaseOrDecreaseLabel() {
switch (Math.sign(this.increaseOrDecrease)) {
case 1:
return 'Increase'
case 0:
return 'Constant'
case -1:
return 'Decrease'
}
},
sign() {
switch (Math.sign(this.increaseOrDecrease)) {
case 1:
return '+'
case 0:
return ''
case -1:
return '-'
}
},
isNullValue() {
return this.value == null
},
formattedValue() {
if (!this.isNullValue) {
return (
this.prefix + Nova.formatNumber(new String(this.value), this.format)
)
}
return ''
},
formattedSuffix() {
if (this.suffixInflection === false) {
return this.suffix
}
return SingularOrPlural(this.value, this.suffix)
},
},
}
</script>