1
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s

This commit is contained in:
SunCheng
2026-01-21 16:09:38 +08:00
parent c2a27abcac
commit b2e903e968
7 changed files with 626 additions and 48 deletions

View File

@@ -101,3 +101,21 @@ export const getDailyStatisticsRange = (params) => {
params
})
}
/**
* 获取累积余额统计数据(用于余额卡片)
* @param {Object} params - 查询参数
* @param {number} params.year - 年份
* @param {number} params.month - 月份
* @returns {Promise<{success: boolean, data: Array}>}
* @returns {Array} data - 每日累积余额列表
* @returns {string} data[].date - 日期
* @returns {number} data[].cumulativeBalance - 累积余额
*/
export const getBalanceStatistics = (params) => {
return request({
url: '/TransactionRecord/GetBalanceStatistics',
method: 'get',
params
})
}

View File

@@ -31,7 +31,7 @@
--chart-color-10: #aab7b8;
--chart-color-11: #ff8ed4;
--chart-color-12: #67e6dc;
--chart-color-13: #ffab73;
--chart-color-13: #5b8dee;
--chart-color-14: #c9b1ff;
--chart-color-15: #7bdff2;
@@ -87,7 +87,6 @@ body {
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',

View File

@@ -11,11 +11,25 @@
(月度)
</div>
</div>
<div class="gauge-wrapper">
<div
ref="monthGaugeRef"
class="chart-body gauge-chart"
:style="{ transform: activeTab === BudgetCategory.Expense ? 'scaleX(-1)' : '' }"
/>
<div class="gauge-text-overlay">
<div
class="remaining-label"
>
{{ activeTab === BudgetCategory.Expense ? '余额' : '差额' }}
</div>
<div
class="remaining-value"
>
¥{{ formatMoney(Math.max(0, overallStats.month.limit - overallStats.month.current)) }}
</div>
</div>
</div>
<div class="gauge-footer compact">
<div class="gauge-item">
<span class="label">
@@ -40,11 +54,25 @@
(年度)
</div>
</div>
<div class="gauge-wrapper">
<div
ref="yearGaugeRef"
class="chart-body gauge-chart"
:style="{ transform: activeTab === BudgetCategory.Expense ? 'scaleX(-1)' : '' }"
/>
<div class="gauge-text-overlay">
<div
class="remaining-label"
>
{{ activeTab === BudgetCategory.Expense ? '余额' : '差额' }}
</div>
<div
class="remaining-value"
>
¥{{ formatMoney(Math.max(0, overallStats.year.limit - overallStats.year.current)) }}
</div>
</div>
</div>
<div class="gauge-footer compact">
<div class="gauge-item">
<span class="label">
@@ -255,12 +283,13 @@ const updateSingleGauge = (chart, data, isExpense) => {
valueAnimation: true,
fontSize: 24, // 字体调小
offsetCenter: [0, -5],
color: 'var(--van-text-color)',
color: getCssVar('--van-text-color'),
formatter: '{value}%',
fontWeight: 'bold',
fontFamily: 'DIN Alternate, system-ui'
fontFamily: 'DIN Alternate, system-ui',
show: false
},
data: [{ value: displayRate }]
data: [{ value: displayRate.toFixed(0) }]
}
]
}
@@ -872,6 +901,42 @@ onUnmounted(() => {
/* 减小内边距 */
}
.gauge-wrapper {
position: relative;
width: 100%;
height: 120px;
}
.gauge-text-overlay {
position: absolute;
bottom: 20%;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
}
.remaining-value {
font-size: 24px;
font-weight: bold;
font-family: DIN Alternate, system-ui;
color: var(--van-text-color);
line-height: 1;
transform-origin: center;
}
.remaining-label {
font-size: 10px;
color: var(--van-text-color-2);
margin-top: 4px;
font-family: system-ui;
transform-origin: center;
}
.chart-header {
margin-bottom: 12px;
}

View File

@@ -77,7 +77,7 @@
</van-tab>
<van-tab
title="存款"
title="计划"
:name="BudgetCategory.Savings"
>
<van-pull-refresh
@@ -108,7 +108,7 @@
</div>
<div class="info-item">
<div class="label">
目标
计划存款
</div>
<div class="value">
¥{{ formatMoney(budget.limit) }}

View File

@@ -9,9 +9,16 @@
/>
<!-- 下拉刷新区域 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-pull-refresh
v-model="refreshing"
@refresh="onRefresh"
>
<!-- 加载提示 -->
<van-loading v-if="loading && !periodicList.length" vertical style="padding: 50px 0">
<van-loading
v-if="loading && !periodicList.length"
vertical
style="padding: 50px 0"
>
加载中...
</van-loading>
@@ -23,10 +30,18 @@
class="periodic-list"
@load="onLoad"
>
<van-cell-group v-for="item in periodicList" :key="item.id" inset class="periodic-item">
<van-cell-group
v-for="item in periodicList"
:key="item.id"
inset
class="periodic-item"
>
<van-swipe-cell>
<div @click="editPeriodic(item)">
<van-cell :title="item.reason || '无摘要'" :label="getPeriodicTypeText(item)">
<van-cell
:title="item.reason || '无摘要'"
:label="getPeriodicTypeText(item)"
>
<template #value>
<div class="amount-info">
<span :class="['amount', item.type === 1 ? 'income' : 'expense']">
@@ -35,7 +50,10 @@
</div>
</template>
</van-cell>
<van-cell title="分类" :value="item.classify || '未分类'" />
<van-cell
title="分类"
:value="item.classify || '未分类'"
/>
<van-cell
title="下次执行时间"
:value="formatDateTime(item.nextExecuteTime) || '未设置'"
@@ -77,7 +95,13 @@
<!-- 底部新增按钮 -->
<div class="bottom-button">
<van-button type="primary" size="large" round icon="plus" @click="openAddDialog">
<van-button
type="primary"
size="large"
round
icon="plus"
@click="openAddDialog"
>
新增周期账单
</van-button>
</div>
@@ -89,7 +113,10 @@
height="75%"
>
<van-form>
<van-cell-group inset title="周期设置">
<van-cell-group
inset
title="周期设置"
>
<van-field
v-model="form.periodicTypeText"
is-link
@@ -150,7 +177,10 @@
/>
</van-cell-group>
<van-cell-group inset title="基本信息">
<van-cell-group
inset
title="基本信息"
>
<van-field
v-model="form.reason"
name="reason"
@@ -170,35 +200,68 @@
type="number"
:rules="[{ required: true, message: '请输入金额' }]"
/>
<van-field v-model="form.type" name="type" label="类型">
<van-field
v-model="form.type"
name="type"
label="类型"
>
<template #input>
<van-radio-group v-model="form.type" direction="horizontal">
<van-radio :name="0"> 支出 </van-radio>
<van-radio :name="1"> 收入 </van-radio>
<van-radio :name="2"> 不计 </van-radio>
<van-radio-group
v-model="form.type"
direction="horizontal"
>
<van-radio :value="0">
支出
</van-radio>
<van-radio :value="1">
收入
</van-radio>
<van-radio :value="2">
不计
</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field name="classify" label="分类">
<van-field
name="classify"
label="分类"
>
<template #input>
<span v-if="!form.classify" style="color: var(--van-gray-5)">请选择交易分类</span>
<span
v-if="!form.classify"
style="color: var(--van-gray-5)"
>请选择交易分类</span>
<span v-else>{{ form.classify }}</span>
</template>
</van-field>
<!-- 分类选择组件 -->
<ClassifySelector v-model="form.classify" :type="form.type" />
<ClassifySelector
v-model="form.classify"
:type="form.type"
/>
</van-cell-group>
</van-form>
<template #footer>
<van-button round block type="primary" :loading="submitting" @click="submit">
<van-button
round
block
type="primary"
:loading="submitting"
@click="submit"
>
{{ isEdit ? '更新' : '确认添加' }}
</van-button>
</template>
</PopupContainer>
<!-- 周期类型选择器 -->
<van-popup v-model:show="showPeriodicTypePicker" position="bottom" round teleport="body">
<van-popup
v-model:show="showPeriodicTypePicker"
position="bottom"
round
teleport="body"
>
<van-picker
:columns="periodicTypeColumns"
@confirm="onPeriodicTypeConfirm"
@@ -207,7 +270,12 @@
</van-popup>
<!-- 星期选择器 -->
<van-popup v-model:show="showWeekdaysPicker" position="bottom" round teleport="body">
<van-popup
v-model:show="showWeekdaysPicker"
position="bottom"
round
teleport="body"
>
<van-picker
:columns="weekdaysColumns"
@confirm="onWeekdaysConfirm"
@@ -216,7 +284,12 @@
</van-popup>
<!-- 日期选择器 -->
<van-popup v-model:show="showMonthDaysPicker" position="bottom" round teleport="body">
<van-popup
v-model:show="showMonthDaysPicker"
position="bottom"
round
teleport="body"
>
<van-picker
:columns="monthDaysColumns"
@confirm="onMonthDaysConfirm"
@@ -233,7 +306,9 @@ import { showToast, showConfirmDialog } from 'vant'
import {
getPeriodicList,
deletePeriodic as deletePeriodicApi,
togglePeriodicEnabled
togglePeriodicEnabled,
createPeriodic,
updatePeriodic
} from '@/api/transactionPeriodic'
import PopupContainer from '@/components/PopupContainer.vue'
import ClassifySelector from '@/components/ClassifySelector.vue'
@@ -429,10 +504,10 @@ const editPeriodic = (item) => {
form.id = item.id
form.reason = item.reason
form.amount = item.amount.toString()
form.type = item.type
form.type = parseInt(item.type)
form.classify = item.classify
form.periodicType = item.periodicType
form.periodicTypeText = periodicTypeColumns.find((t) => t.value === item.periodicType)?.text || ''
form.periodicType = parseInt(item.periodicType)
form.periodicTypeText = periodicTypeColumns.find((t) => t.value === parseInt(item.periodicType))?.text || ''
// 解析周期配置
if (item.periodicConfig) {
@@ -557,6 +632,105 @@ const onMonthDaysConfirm = ({ selectedValues, selectedOptions }) => {
form.monthDaysText = selectedOptions[0].text
showMonthDaysPicker.value = false
}
// 提交表单
const submit = async () => {
// 验证基本字段
if (!form.reason.trim()) {
showToast('请输入摘要')
return
}
if (!form.amount) {
showToast('请输入金额')
return
}
if (form.amount < 0) {
showToast('金额必须大于0')
return
}
if (!form.classify) {
showToast('请选择分类')
return
}
// 验证周期配置
if (!form.periodicTypeText) {
showToast('请选择周期类型')
return
}
let periodicConfig = ''
switch (form.periodicType) {
case 0: // 每天
periodicConfig = ''
break
case 1: // 每周
if (form.weekdays.length === 0) {
showToast('请选择星期几')
return
}
periodicConfig = form.weekdays.join(',')
break
case 2: // 每月
if (form.monthDays.length === 0) {
showToast('请选择日期')
return
}
periodicConfig = form.monthDays.join(',')
break
case 3: // 每季度
if (!form.quarterDay) {
showToast('请输入季度开始后第几天')
return
}
periodicConfig = form.quarterDay.toString()
break
case 4: // 每年
if (!form.yearDay) {
showToast('请输入年开始后第几天')
return
}
periodicConfig = form.yearDay.toString()
break
}
submitting.value = true
try {
const requestData = {
periodicType: parseInt(form.periodicType),
periodicConfig: periodicConfig,
amount: parseFloat(form.amount),
type: parseInt(form.type),
classify: form.classify,
reason: form.reason.trim()
}
let response
if (isEdit.value) {
// 更新
requestData.id = form.id
requestData.isEnabled = true // Update API 需要此字段
response = await updatePeriodic(requestData)
} else {
// 创建
response = await createPeriodic(requestData)
}
if (response.success) {
showToast(isEdit.value ? '更新成功' : '添加成功')
dialogVisible.value = false
resetForm()
loadData(true)
} else {
showToast(response.message || (isEdit.value ? '更新失败' : '添加失败'))
}
} catch (error) {
console.error('提交出错:', error)
showToast(isEdit.value ? '更新出错' : '添加出错')
} finally {
submitting.value = false
}
}
</script>
<style scoped>

View File

@@ -42,12 +42,51 @@
class="statistics-content"
>
<div>
<!-- 余额卡片 -->
<div
class="common-card"
style="margin-top: 12px;"
>
<div
class="card-header"
style="padding-bottom: 0;"
>
<h3 class="card-title">
余额
</h3>
</div>
<!-- 余额金额 -->
<div class="balance-amount">
<span
class="balance-value"
:class="{ 'balance-positive': displayBalance >= 0, 'balance-negative': displayBalance < 0 }"
>
¥{{ formatMoney(displayBalance) }}
</span>
</div>
<!-- 余额变化图表 -->
<div
class="balance-chart"
style="height: 130px; padding: 0"
>
<div
ref="balanceChartRef"
style="width: 100%; height: 100%"
/>
</div>
</div>
<!-- 趋势统计 -->
<div
class="common-card"
style="padding-bottom: 5px; margin-top: 12px;"
>
<div class="card-header">
<div
class="card-header"
style="padding-bottom: 0;"
>
<h3 class="card-title">
收支趋势
</h3>
@@ -288,7 +327,7 @@ import { ref, computed, onMounted, onActivated, nextTick, watch } from 'vue'
import { onBeforeUnmount } from 'vue'
import { showToast } from 'vant'
import { useRouter } from 'vue-router'
import { getMonthlyStatistics, getCategoryStatistics, getDailyStatistics } from '@/api/statistics'
import { getMonthlyStatistics, getCategoryStatistics, getDailyStatistics, getBalanceStatistics } from '@/api/statistics'
import * as echarts from 'echarts'
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue'
@@ -392,10 +431,14 @@ const noneCategoriesView = computed(() => {
// 趋势数据
const dailyData = ref([])
// 余额数据(独立)
const balanceData = ref([])
const chartRef = ref(null)
const pieChartRef = ref(null)
const balanceChartRef = ref(null)
let chartInstance = null
let pieChartInstance = null
let balanceChartInstance = null
// 日期范围
const minDate = new Date(2020, 0, 1)
@@ -425,6 +468,50 @@ const isUnclassified = computed(() => {
return selectedClassify.value === '未分类' || selectedClassify.value === ''
})
// 当月累积余额
const currentMonthBalance = computed(() => {
if (balanceData.value.length === 0) {
return 0
}
// 获取最后一天的累积余额
return balanceData.value[balanceData.value.length - 1].cumulativeBalance || 0
})
// 显示的动画余额
const displayBalance = ref(0)
// 监听余额变化,执行动画
watch(currentMonthBalance, (newVal, oldVal) => {
if (oldVal === undefined) {
// 初始加载,直接设置,不需要动画
displayBalance.value = newVal
return
}
// 数字跳动动画
const duration = 800 // 动画持续时间(毫秒)
const startValue = oldVal
const endValue = newVal
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
// 使用缓动函数easeOutQuad
const easeProgress = 1 - (1 - progress) * (1 - progress)
displayBalance.value = Math.round(startValue + (endValue - startValue) * easeProgress)
if (progress < 1) {
requestAnimationFrame(animate)
} else {
displayBalance.value = endValue
}
}
animate()
})
// 格式化金额
const formatMoney = (value) => {
if (!value && value !== 0) {
@@ -463,7 +550,7 @@ const fetchStatistics = async (showLoading = true) => {
}
try {
await Promise.all([fetchMonthlyData(), fetchCategoryData(), fetchDailyData()])
await Promise.all([fetchMonthlyData(), fetchCategoryData(), fetchDailyData(), fetchBalanceData()])
} catch (error) {
console.error('获取统计数据失败:', error)
showToast('获取统计数据失败')
@@ -474,6 +561,7 @@ const fetchStatistics = async (showLoading = true) => {
nextTick(() => {
renderChart(dailyData.value)
renderPieChart()
renderBalanceChart()
})
}
}
@@ -576,6 +664,23 @@ const fetchDailyData = async () => {
}
}
// 获取余额统计数据(独立接口)
const fetchBalanceData = async () => {
try {
const response = await getBalanceStatistics({
year: currentYear.value,
month: currentMonth.value
})
if (response.success && response.data) {
balanceData.value = response.data
}
} catch (error) {
console.error('获取余额统计数据失败:', error)
showToast('获取余额统计数据失败')
}
}
const renderChart = (data) => {
if (!chartRef.value) {
return
@@ -670,6 +775,12 @@ const renderChart = (data) => {
return accumulatedBalance
})
const legendData = [
{ name: '支出', value: '¥' + formatMoney(expenses[expenses.length - 1]) },
{ name: '收入', value: '¥' + formatMoney(incomes[incomes.length - 1]) },
{ name: '存款', value: '¥' + formatMoney(balances[balances.length - 1]) }
]
const option = {
tooltip: {
trigger: 'axis',
@@ -682,10 +793,14 @@ const renderChart = (data) => {
}
},
legend: {
data: ['支出', '收入', '存款'],
data: legendData.map((item) => item.name),
bottom: 0,
textStyle: {
color: getCssVar('--chart-text-muted') // 适配深色模式
},
formatter: function (name) {
const item = legendData.find((d) => d.name === name)
return item ? `${name} ${item.value}` : name
}
},
grid: {
@@ -876,6 +991,121 @@ const renderPieChart = () => {
pieChartInstance.setOption(option)
}
// 渲染余额变化图表
const renderBalanceChart = () => {
if (!balanceChartRef.value) {
return
}
if (balanceData.value.length === 0) {
return
}
// 尝试获取DOM上的现有实例
const existingInstance = echarts.getInstanceByDom(balanceChartRef.value)
if (balanceChartInstance && balanceChartInstance !== existingInstance) {
if (!balanceChartInstance.isDisposed()) {
balanceChartInstance.dispose()
}
balanceChartInstance = null
}
if (balanceChartInstance && balanceChartInstance.getDom() !== balanceChartRef.value) {
balanceChartInstance.dispose()
balanceChartInstance = null
}
if (!balanceChartInstance && existingInstance) {
balanceChartInstance = existingInstance
}
if (!balanceChartInstance) {
balanceChartInstance = echarts.init(balanceChartRef.value)
}
const dates = balanceData.value.map((item) => {
const date = new Date(item.date)
return `${date.getMonth() + 1}/${date.getDate()}`
})
const balances = balanceData.value.map((item) => item.cumulativeBalance)
const option = {
tooltip: {
trigger: 'axis',
formatter: function (params) {
if (params.length === 0) {
return ''
}
const param = params[0]
return `${param.name}<br/>余额: ¥${formatMoney(param.value)}`
}
},
grid: {
left: '3%',
right: '4%',
bottom: '5%',
top: '5%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates,
axisLabel: {
color: getCssVar('--chart-text-muted'),
fontSize: 11
}
},
yAxis: {
type: 'value',
splitNumber: 4,
axisLabel: {
color: getCssVar('--chart-text-muted'),
fontSize: 11,
formatter: (value) => {
return value / 1000 + 'k'
}
},
splitLine: {
lineStyle: {
type: 'dashed',
color: getCssVar('--van-border-color')
}
}
},
series: [
{
name: '余额',
type: 'line',
data: balances,
itemStyle: { color: getCssVar('--chart-color-13') },
showSymbol: false,
smooth: true,
lineStyle: { width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: getCssVar('--chart-color-13')
},
{
offset: 1,
color: getCssVar('--chart-color-13')
}
])
}
}
]
}
balanceChartInstance.setOption(option)
// 设置图表透明度
if (balanceChartRef.value) {
balanceChartRef.value.style.opacity = '0.85'
}
}
// 跳转到智能分析页面
const goToAnalysis = () => {
router.push('/bill-analysis')
@@ -1061,6 +1291,7 @@ onMounted(() => {
const handleResize = () => {
chartInstance && chartInstance.resize()
pieChartInstance && pieChartInstance.resize()
balanceChartInstance && balanceChartInstance.resize()
}
// 监听DOM引用变化确保在月份切换DOM重建后重新渲染图表
@@ -1085,6 +1316,15 @@ watch(pieChartRef, (newVal) => {
}
})
watch(balanceChartRef, (newVal) => {
if (newVal) {
setTimeout(() => {
renderBalanceChart()
balanceChartInstance && balanceChartInstance.resize()
}, 50)
}
})
// 页面激活时刷新数据(从其他页面返回时)
onActivated(() => {
fetchStatistics()
@@ -1105,6 +1345,7 @@ onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
chartInstance && chartInstance.dispose()
pieChartInstance && pieChartInstance.dispose()
balanceChartInstance && balanceChartInstance.dispose()
})
const onGlobalTransactionsChanged = () => {
@@ -1145,6 +1386,37 @@ onBeforeUnmount(() => {
color: var(--van-text-color);
}
/* 余额卡片 */
.balance-amount {
text-align: center;
padding: 9px 0 8px 0;
font-size: 32px;
font-weight: bold;
color: var(--van-text-color);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text",
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.balance-value {
color: var(--chart-color-13);
transition: color 0.3s ease;
}
.balance-value.balance-positive {
color: var(--van-success-color);
}
.balance-value.balance-negative {
color: var(--van-danger-color);
}
.balance-chart {
border-radius: 8px;
background: rgba(0, 0, 0, 0.02);
padding: 10px 0;
margin: 0 -12px;
}
/* 环形图 */
.chart-container {
padding: 0;
@@ -1411,8 +1683,8 @@ onBeforeUnmount(() => {
padding: 12px;
}
.side-by-side-cards .card-header {
margin-bottom: 12px;
.card-header {
margin-bottom: 0;
}
.text-ellipsis {

View File

@@ -252,6 +252,48 @@ public class TransactionRecordController(
}
}
/// <summary>
/// 获取累积余额统计数据(用于余额卡片图表)
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<BalanceStatisticsDto>>> GetBalanceStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
// 获取存款分类
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
// 获取每日统计数据
var statistics = await transactionRepository.GetDailyStatisticsAsync(year, month, savingClassify);
// 按日期排序并计算累积余额
var sortedStats = statistics.OrderBy(s => s.Key).ToList();
var result = new List<BalanceStatisticsDto>();
decimal cumulativeBalance = 0;
foreach (var item in sortedStats)
{
decimal dailyBalance = item.Value.income - item.Value.expense;
cumulativeBalance += dailyBalance;
result.Add(new BalanceStatisticsDto(
item.Key,
cumulativeBalance
));
}
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取累积余额统计失败,年份: {Year}, 月份: {Month}", year, month);
return $"获取累积余额统计失败: {ex.Message}".Fail<List<BalanceStatisticsDto>>();
}
}
/// <summary>
/// 获取指定月份每天的消费统计
/// </summary>
@@ -311,7 +353,7 @@ public class TransactionRecordController(
s.Value.count,
s.Value.expense,
s.Value.income,
s.Value.income - s.Value.expense
s.Value.saving
)).ToList();
return result.Ok();
@@ -778,6 +820,14 @@ public record DailyStatisticsDto(
decimal Balance
);
/// <summary>
/// 累积余额统计DTO
/// </summary>
public record BalanceStatisticsDto(
string Date,
decimal CumulativeBalance
);
/// <summary>
/// 智能分类请求DTO
/// </summary>