cl-text.uvue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <template>
  2. <!-- #ifdef MP -->
  3. <view
  4. class="cl-text"
  5. :class="[
  6. isDark ? 'text-surface-50' : 'text-surface-700',
  7. {
  8. 'truncate w-full': ellipsis
  9. },
  10. {
  11. '!text-primary-500': color == 'primary',
  12. '!text-green-500': color == 'success',
  13. '!text-yellow-500': color == 'warn',
  14. '!text-red-500': color == 'error',
  15. [isDark ? '!text-surface-300' : '!text-surface-500']: color == 'info',
  16. '!text-surface-700': color == 'dark',
  17. '!text-surface-50': color == 'light',
  18. '!text-surface-400': color == 'disabled'
  19. },
  20. pt.className
  21. ]"
  22. :style="textStyle"
  23. :selectable="selectable"
  24. :space="space"
  25. :decode="decode"
  26. :key="cache.key"
  27. >
  28. <slot>{{ content }}</slot>
  29. </view>
  30. <!-- #endif -->
  31. <!-- #ifndef MP -->
  32. <text
  33. class="cl-text"
  34. :class="[
  35. isDark ? 'text-surface-50' : 'text-surface-700',
  36. {
  37. 'truncate w-full': ellipsis
  38. },
  39. {
  40. '!text-primary-500': color == 'primary',
  41. '!text-green-500': color == 'success',
  42. '!text-yellow-500': color == 'warn',
  43. '!text-red-500': color == 'error',
  44. [isDark ? '!text-surface-300' : '!text-surface-500']: color == 'info',
  45. '!text-surface-700': color == 'dark',
  46. '!text-surface-50': color == 'light',
  47. '!text-surface-400': color == 'disabled'
  48. },
  49. pt.className
  50. ]"
  51. :style="textStyle"
  52. :selectable="selectable"
  53. :space="space"
  54. :decode="decode"
  55. :key="cache.key"
  56. >
  57. <slot>{{ content }}</slot>
  58. </text>
  59. <!-- #endif -->
  60. </template>
  61. <script setup lang="ts">
  62. import { computed, type PropType } from "vue";
  63. import { isDark, parsePt, useCache } from "@/cool";
  64. import type { ClTextType } from "../../types";
  65. defineOptions({
  66. name: "cl-text"
  67. });
  68. // 组件属性定义
  69. const props = defineProps({
  70. // 透传样式
  71. pt: {
  72. type: Object,
  73. default: () => ({})
  74. },
  75. // 显示的值
  76. value: {
  77. type: [String, Number] as PropType<string | number | null>,
  78. default: null
  79. },
  80. // 文本颜色
  81. color: {
  82. type: String,
  83. default: ""
  84. },
  85. // 文本类型
  86. type: {
  87. type: String as PropType<ClTextType>,
  88. default: "default"
  89. },
  90. // 是否开启脱敏/加密
  91. mask: {
  92. type: Boolean,
  93. default: false
  94. },
  95. // 金额货币符号
  96. currency: {
  97. type: String,
  98. default: "¥"
  99. },
  100. // 金额小数位数
  101. precision: {
  102. type: Number,
  103. default: 2
  104. },
  105. // 脱敏起始位置
  106. maskStart: {
  107. type: Number,
  108. default: 3
  109. },
  110. // 脱敏结束位置
  111. maskEnd: {
  112. type: Number,
  113. default: 4
  114. },
  115. // 脱敏替换字符
  116. maskChar: {
  117. type: String,
  118. default: "*"
  119. },
  120. // 是否省略号
  121. ellipsis: {
  122. type: Boolean,
  123. default: false
  124. },
  125. // 是否可选择
  126. selectable: {
  127. type: Boolean,
  128. default: false
  129. },
  130. // 显示连续空格
  131. space: {
  132. type: String as PropType<"ensp" | "emsp" | "nbsp">,
  133. default: ""
  134. },
  135. // 是否解码 (app平台如需解析字符实体,需要配置为 true)
  136. decode: {
  137. type: Boolean,
  138. default: false
  139. }
  140. });
  141. // 缓存
  142. const { cache } = useCache(() => [props.color, props]);
  143. // 透传样式类型
  144. type PassThrough = {
  145. className?: string;
  146. };
  147. // 解析透传样式
  148. const pt = computed(() => parsePt<PassThrough>(props.pt));
  149. // 文本样式
  150. const textStyle = computed(() => {
  151. const style = new Map<string, string>();
  152. if (props.color != "") {
  153. style.set("color", props.color);
  154. }
  155. return style;
  156. });
  157. /**
  158. * 手机号脱敏处理
  159. * 保留前3位和后4位,中间4位替换为掩码
  160. */
  161. function formatPhone(phone: string): string {
  162. if (phone.length != 11 || !props.mask) return phone;
  163. return phone.replace(/(\d{3})\d{4}(\d{4})/, `$1${props.maskChar.repeat(4)}$2`);
  164. }
  165. /**
  166. * 姓名脱敏处理
  167. * 2个字时保留第1个字
  168. * 大于2个字时保留首尾字
  169. */
  170. function formatName(name: string): string {
  171. if (name.length <= 1 || !props.mask) return name;
  172. if (name.length == 2) {
  173. return name[0] + props.maskChar;
  174. }
  175. return name[0] + props.maskChar.repeat(name.length - 2) + name[name.length - 1];
  176. }
  177. /**
  178. * 金额格式化
  179. * 1. 处理小数位数
  180. * 2. 添加千分位分隔符
  181. * 3. 添加货币符号
  182. */
  183. function formatAmount(amount: string | number): string {
  184. let num: number;
  185. if (typeof amount == "number") {
  186. num = amount;
  187. } else {
  188. num = parseFloat(amount);
  189. }
  190. const formatted = num.toFixed(props.precision);
  191. const parts = formatted.split(".");
  192. parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  193. return props.currency + parts.join(".");
  194. }
  195. /**
  196. * 银行卡号脱敏
  197. * 保留开头和结尾指定位数,中间用掩码替换
  198. */
  199. function formatCard(card: string): string {
  200. if (card.length < 8 || !props.mask) return card;
  201. const start = card.substring(0, props.maskStart);
  202. const end = card.substring(card.length - props.maskEnd);
  203. const middle = props.maskChar.repeat(card.length - props.maskStart - props.maskEnd);
  204. return start + middle + end;
  205. }
  206. /**
  207. * 邮箱脱敏处理
  208. * 保留用户名首尾字符和完整域名
  209. */
  210. function formatEmail(email: string): string {
  211. if (!props.mask) return email;
  212. const atIndex = email.indexOf("@");
  213. if (atIndex == -1) return email;
  214. const username = email.substring(0, atIndex);
  215. const domain = email.substring(atIndex);
  216. if (username.length <= 2) return email;
  217. const maskedUsername =
  218. username[0] + props.maskChar.repeat(username.length - 2) + username[username.length - 1];
  219. return maskedUsername + domain;
  220. }
  221. /**
  222. * 根据不同类型格式化显示
  223. */
  224. const content = computed(() => {
  225. const val = props.value ?? "";
  226. switch (props.type) {
  227. case "phone":
  228. return formatPhone(val as string);
  229. case "name":
  230. return formatName(val as string);
  231. case "amount":
  232. return formatAmount(val as number);
  233. case "card":
  234. return formatCard(val as string);
  235. case "email":
  236. return formatEmail(val as string);
  237. default:
  238. return val;
  239. }
  240. });
  241. </script>
  242. <style lang="scss" scoped>
  243. .cl-text {
  244. @apply text-md;
  245. }
  246. </style>