cl-text.uvue 7.1 KB

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