cl-text.uvue 6.3 KB

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