u-amount.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <template>
  2. <view class="u-amount" :style="[$u.addStyle(customStyle)]">
  3. <!-- 插槽模式 -->
  4. <slot v-if="$slots.default || $slots.$default" :integer="integerPart" :decimal="decimalPart" :capital="capitalText"></slot>
  5. <!-- 默认显示模式 -->
  6. <template v-else>
  7. <!-- 大写中文模式 -->
  8. <text v-if="capital" class="u-amount__capital" :style="[textStyle]">{{ capitalText }}</text>
  9. <!-- 普通数字模式 -->
  10. <template v-else>
  11. <!-- 前置符号 -->
  12. <text v-if="showSymbol && !reverse" class="u-amount__symbol" :style="[symbolStyle]">{{ symbol }}</text>
  13. <!-- 整数部分 -->
  14. <text class="u-amount__integer" :style="[integerStyle]">{{ formattedInteger }}</text>
  15. <!-- 小数部分 -->
  16. <text v-if="showDecimal && decimalPart" class="u-amount__decimal" :style="[decimalStyle]">.{{ decimalPart }}</text>
  17. <!-- 后置符号 -->
  18. <text v-if="showSymbol && reverse" class="u-amount__symbol" :style="[symbolStyle]">{{ symbol }}</text>
  19. </template>
  20. </template>
  21. </view>
  22. </template>
  23. <script>
  24. import props from './props.js';
  25. import mixin from '../../libs/mixin/mixin'
  26. import mpMixin from '../../libs/mixin/mpMixin';
  27. /**
  28. * amount 金融数字
  29. * @description 用于展示金额、价格等,可以转换为人民币大写,支持动画效果
  30. * @tutorial https://uviewui.com/components/amount.html
  31. *
  32. * @property {Number} v-model/modelValue 金额数值 (默认 0 )
  33. * @property {String} symbol 金融符号 (默认 '¥' )
  34. * @property {Boolean} reverse 是否置后金额符号位置 (默认 false )
  35. * @property {Number} precision 数字精度,小数点后保留几位 (默认 2 )
  36. * @property {Boolean} roundUp 数字精度取舍是否四舍五入 (默认 true )
  37. * @property {Boolean} transition 数字变化是否使用动画 (默认 false )
  38. * @property {Number} duration 数字变化动画时长 (默认 1000 )
  39. * @property {Number} separatorDigits 分隔符位置 (默认 4 )
  40. * @property {String} separator 分隔符 (默认 ',' )
  41. * @property {Boolean} showSymbol 是否显示金融符号 (默认 true )
  42. * @property {Boolean} showDecimal 是否显示小数 (默认 true )
  43. * @property {Boolean} showSeparator 是否显示分隔符 (默认 false )
  44. * @property {Boolean} capital 数字是否转换为大写中文 (默认 false )
  45. * @property {String} fontSize 整数字体大小 (默认 '' )
  46. * @property {String|Number} fontSizeRatio 金融符号小数字体大小 (默认 0.7 )
  47. * @property {String} color 颜色,支持主题色 (默认 '' )
  48. * @property {String} colorRatio 金融符号与小数字体颜色,支持主题色
  49. * @property {Object} customStyle 定义需要用到的外部样式
  50. * @example <u-amount v-model="123456.789" :show-separator="true" />
  51. */
  52. export default {
  53. name: 'u-amount',
  54. mixins: [mpMixin, mixin, props],
  55. data() {
  56. return {
  57. displayValue: 0,
  58. animationTimer: null
  59. }
  60. },
  61. computed: {
  62. // 格式化后的数值
  63. formattedValue() {
  64. let num = this.displayValue;
  65. if (this.roundUp) {
  66. num = Math.round(num * Math.pow(10, this.precision)) / Math.pow(10, this.precision);
  67. } else {
  68. num = Math.floor(num * Math.pow(10, this.precision)) / Math.pow(10, this.precision);
  69. }
  70. return num.toFixed(this.precision);
  71. },
  72. // 整数部分
  73. integerPart() {
  74. const parts = this.formattedValue.split('.');
  75. return parts[0];
  76. },
  77. // 小数部分
  78. decimalPart() {
  79. const parts = this.formattedValue.split('.');
  80. return parts[1] || '';
  81. },
  82. // 格式化的整数(带分隔符)
  83. formattedInteger() {
  84. if (!this.showSeparator) return this.integerPart;
  85. return this.addSeparator(this.integerPart);
  86. },
  87. // 中文大写
  88. capitalText() {
  89. if (!this.capital) return '';
  90. return this.toChinese(this.displayValue);
  91. },
  92. // 文本通用样式
  93. textStyle() {
  94. const style = {};
  95. if (this.color){
  96. style.color = this.$uColor('color');
  97. }
  98. if (this.fontSize){
  99. style.fontSize = this.fontSize;
  100. }
  101. return style;
  102. },
  103. // 符号样式
  104. symbolStyle() {
  105. const style = {};
  106. if (this.color){
  107. style.color = this.$uColor('color');
  108. }
  109. if (this.colorRatio){
  110. style.color = this.$uColor('colorRatio');
  111. }
  112. if (this.fontSize) {
  113. if (this.fontSizeRatio) {
  114. style.fontSize = this.fontSizeRatio;
  115. } else {
  116. const size = parseFloat(this.fontSize);
  117. const ratio = parseFloat(this.fontSizeRatio);
  118. style.fontSize = uni.$u.addUnit(size * ratio);
  119. }
  120. }
  121. return style;
  122. },
  123. // 整数样式
  124. integerStyle() {
  125. const style = {};
  126. if (this.color){
  127. style.color = this.$uColor('color');
  128. }
  129. if (this.fontSize){
  130. style.fontSize = this.fontSize;
  131. }
  132. return style;
  133. },
  134. // 小数样式
  135. decimalStyle() {
  136. const style = {};
  137. if (this.color){
  138. style.color = this.$uColor('color');
  139. }
  140. if (this.colorRatio){
  141. style.color = this.$uColor('colorRatio');
  142. }
  143. if (this.fontSize) {
  144. if (this.fontSizeRatio) {
  145. style.fontSize = this.fontSizeRatio;
  146. } else {
  147. const size = parseFloat(this.fontSize);
  148. const ratio = parseFloat(this.fontSizeRatio);
  149. style.fontSize = uni.$u.addUnit((size * ratio));
  150. }
  151. }
  152. return style;
  153. }
  154. },
  155. watch: {
  156. value: {
  157. immediate: true,
  158. handler(newVal) {
  159. if(!newVal) return;
  160. if (this.transition) {
  161. this.startAnimation(newVal);
  162. } else {
  163. this.displayValue = newVal;
  164. }
  165. }
  166. }
  167. },
  168. methods: {
  169. // 添加分隔符
  170. addSeparator(num) {
  171. const str = num.toString();
  172. const len = str.length;
  173. let result = '';
  174. for (let i = 0; i < len; i++) {
  175. if (i > 0 && (len - i) % this.separatorDigits === 0) {
  176. result += this.separator;
  177. }
  178. result += str[i];
  179. }
  180. return result;
  181. },
  182. // 启动动画
  183. startAnimation(targetValue) {
  184. if (this.animationTimer) {
  185. clearInterval(this.animationTimer);
  186. }
  187. const startValue = this.displayValue;
  188. const diff = targetValue - startValue;
  189. const stepTime = 16; // 60fps
  190. const steps = this.duration / stepTime;
  191. const stepValue = diff / steps;
  192. let currentStep = 0;
  193. this.animationTimer = setInterval(() => {
  194. currentStep++;
  195. if (currentStep >= steps) {
  196. this.displayValue = targetValue;
  197. clearInterval(this.animationTimer);
  198. this.animationTimer = null;
  199. } else {
  200. this.displayValue = startValue + (stepValue * currentStep);
  201. }
  202. }, stepTime);
  203. },
  204. // 转换为中文大写
  205. toChinese(amount) {
  206. // 小数单位:角、分、毫、厘
  207. const fractionUnits = ['角', '分', '毫', '厘'];
  208. // 数字对应的中文大写
  209. const chineseDigits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
  210. // 整数单位
  211. const integerUnits = [
  212. ['元', '万', '亿'],
  213. ['', '拾', '佰', '仟']
  214. ];
  215. // 处理负数
  216. const prefix = amount < 0 ? '欠' : '';
  217. amount = Math.abs(amount);
  218. let result = '';
  219. // 处理小数部分(角、分、毫、厘)
  220. for (let i = 0; i < fractionUnits.length; i++) {
  221. const decimalDigit = Math.floor(amount * Math.pow(10, i + 1)) % 10;
  222. if (decimalDigit > 0) {
  223. result += chineseDigits[decimalDigit] + fractionUnits[i];
  224. }
  225. }
  226. // 如果小数部分为空,则显示"整"
  227. result = result || '整';
  228. // 处理整数部分
  229. let integerPart = Math.floor(amount);
  230. for (let i = 0; i < integerUnits[0].length && integerPart > 0; i++) {
  231. let sectionResult = '';
  232. for (let j = 0; j < integerUnits[1].length && integerPart > 0; j++) {
  233. sectionResult = chineseDigits[integerPart % 10] + integerUnits[1][j] + sectionResult;
  234. integerPart = Math.floor(integerPart / 10);
  235. }
  236. result = sectionResult.replace(/(零.)*零$/, '').replace(/^$/, '零') + integerUnits[0][i] + result;
  237. }
  238. // 处理特殊情况和格式化
  239. return prefix + result.replace(/(零.)*零元/, '元')
  240. .replace(/(零.)+/g, '零')
  241. .replace(/^整$/, '零元整')
  242. .replace(/零角零分零毫零厘/, '整')
  243. .replace(/零角零分零毫/, '')
  244. .replace(/零角零分/, '')
  245. .replace(/零角/, '');
  246. }
  247. },
  248. // #ifdef VUE2
  249. beforeDestroy() {
  250. // #endif
  251. // #ifdef VUE3
  252. beforeUnmount() {
  253. // #endif
  254. if (this.animationTimer) {
  255. clearInterval(this.animationTimer);
  256. }
  257. }
  258. }
  259. </script>
  260. <style lang="scss" scoped>
  261. @import "../../libs/css/components.scss";
  262. .u-amount {
  263. @include flex(row);
  264. align-items: baseline;
  265. &__symbol {
  266. font-weight: normal;
  267. }
  268. &__integer {
  269. font-weight: bold;
  270. }
  271. &__decimal {
  272. font-weight: normal;
  273. }
  274. &__capital {
  275. font-weight: normal;
  276. }
  277. }
  278. </style>