cl-text.uvue 6.5 KB

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